autoglm-gui 0.3.1__py3-none-any.whl → 0.4.1__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/__main__.py +6 -2
- AutoGLM_GUI/adb_plus/__init__.py +8 -1
- AutoGLM_GUI/adb_plus/screenshot.py +1 -4
- AutoGLM_GUI/adb_plus/touch.py +92 -0
- AutoGLM_GUI/api/__init__.py +66 -0
- AutoGLM_GUI/api/agents.py +231 -0
- AutoGLM_GUI/api/control.py +111 -0
- AutoGLM_GUI/api/devices.py +29 -0
- AutoGLM_GUI/api/media.py +163 -0
- AutoGLM_GUI/schemas.py +127 -0
- AutoGLM_GUI/scrcpy_stream.py +65 -28
- AutoGLM_GUI/server.py +2 -491
- AutoGLM_GUI/state.py +33 -0
- AutoGLM_GUI/static/assets/{about-C71SI8ZQ.js → about-gHEqXVMQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-6a-qTECg.js +25 -0
- AutoGLM_GUI/static/assets/index-C8KPPfxe.js +10 -0
- AutoGLM_GUI/static/assets/index-D2-3f619.css +1 -0
- AutoGLM_GUI/static/assets/{index-DUCan6m6.js → index-DgzeSwgt.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/version.py +8 -0
- {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/METADATA +64 -9
- autoglm_gui-0.4.1.dist-info/RECORD +44 -0
- phone_agent/adb/connection.py +0 -1
- phone_agent/adb/device.py +0 -2
- phone_agent/adb/input.py +0 -1
- phone_agent/adb/screenshot.py +0 -1
- phone_agent/agent.py +1 -1
- AutoGLM_GUI/static/assets/chat-C6WtEfKW.js +0 -14
- AutoGLM_GUI/static/assets/index-Dd1xMRCa.css +0 -1
- AutoGLM_GUI/static/assets/index-RqglIZxV.js +0 -10
- autoglm_gui-0.3.1.dist-info/RECORD +0 -35
- {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/media.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Media routes: screenshot, video stream, stream reset."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|
6
|
+
|
|
7
|
+
from AutoGLM_GUI.adb_plus import capture_screenshot
|
|
8
|
+
from AutoGLM_GUI.schemas import ScreenshotRequest, ScreenshotResponse
|
|
9
|
+
from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
|
|
10
|
+
from AutoGLM_GUI.state import scrcpy_locks, scrcpy_streamers
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post("/api/video/reset")
|
|
16
|
+
async def reset_video_stream(device_id: str | None = None) -> dict:
|
|
17
|
+
"""Reset video stream (cleanup scrcpy server,多设备支持)."""
|
|
18
|
+
if device_id:
|
|
19
|
+
if device_id in scrcpy_locks:
|
|
20
|
+
async with scrcpy_locks[device_id]:
|
|
21
|
+
if device_id in scrcpy_streamers:
|
|
22
|
+
print(f"[video/reset] Stopping streamer for device {device_id}")
|
|
23
|
+
scrcpy_streamers[device_id].stop()
|
|
24
|
+
del scrcpy_streamers[device_id]
|
|
25
|
+
print(f"[video/reset] Streamer reset for device {device_id}")
|
|
26
|
+
return {
|
|
27
|
+
"success": True,
|
|
28
|
+
"message": f"Video stream reset for device {device_id}",
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
"success": True,
|
|
32
|
+
"message": f"No active video stream for device {device_id}",
|
|
33
|
+
}
|
|
34
|
+
return {"success": True, "message": f"No video stream for device {device_id}"}
|
|
35
|
+
|
|
36
|
+
device_ids = list(scrcpy_streamers.keys())
|
|
37
|
+
for dev_id in device_ids:
|
|
38
|
+
if dev_id in scrcpy_locks:
|
|
39
|
+
async with scrcpy_locks[dev_id]:
|
|
40
|
+
if dev_id in scrcpy_streamers:
|
|
41
|
+
scrcpy_streamers[dev_id].stop()
|
|
42
|
+
del scrcpy_streamers[dev_id]
|
|
43
|
+
print("[video/reset] All streamers reset")
|
|
44
|
+
return {"success": True, "message": "All video streams reset"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("/api/screenshot", response_model=ScreenshotResponse)
|
|
48
|
+
def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
|
|
49
|
+
"""获取设备截图。此操作无副作用,不影响 PhoneAgent 运行。"""
|
|
50
|
+
try:
|
|
51
|
+
screenshot = capture_screenshot(device_id=request.device_id)
|
|
52
|
+
return ScreenshotResponse(
|
|
53
|
+
success=True,
|
|
54
|
+
image=screenshot.base64_data,
|
|
55
|
+
width=screenshot.width,
|
|
56
|
+
height=screenshot.height,
|
|
57
|
+
is_sensitive=screenshot.is_sensitive,
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
return ScreenshotResponse(
|
|
61
|
+
success=False,
|
|
62
|
+
image="",
|
|
63
|
+
width=0,
|
|
64
|
+
height=0,
|
|
65
|
+
is_sensitive=False,
|
|
66
|
+
error=str(e),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.websocket("/api/video/stream")
|
|
71
|
+
async def video_stream_ws(websocket: WebSocket, device_id: str | None = None):
|
|
72
|
+
"""Stream real-time H.264 video from scrcpy server via WebSocket(多设备支持)."""
|
|
73
|
+
await websocket.accept()
|
|
74
|
+
|
|
75
|
+
if not device_id:
|
|
76
|
+
await websocket.send_json({"error": "device_id is required"})
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
print(f"[video/stream] WebSocket connection for device {device_id}")
|
|
80
|
+
|
|
81
|
+
if device_id not in scrcpy_locks:
|
|
82
|
+
scrcpy_locks[device_id] = asyncio.Lock()
|
|
83
|
+
|
|
84
|
+
async with scrcpy_locks[device_id]:
|
|
85
|
+
if device_id not in scrcpy_streamers:
|
|
86
|
+
print(f"[video/stream] Creating streamer for device {device_id}")
|
|
87
|
+
scrcpy_streamers[device_id] = ScrcpyStreamer(
|
|
88
|
+
device_id=device_id, max_size=1280, bit_rate=4_000_000
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
print(f"[video/stream] Starting scrcpy server for device {device_id}")
|
|
93
|
+
await scrcpy_streamers[device_id].start()
|
|
94
|
+
print(f"[video/stream] Scrcpy server started for device {device_id}")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
import traceback
|
|
97
|
+
|
|
98
|
+
print(f"[video/stream] Failed to start streamer: {e}")
|
|
99
|
+
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
100
|
+
scrcpy_streamers[device_id].stop()
|
|
101
|
+
del scrcpy_streamers[device_id]
|
|
102
|
+
try:
|
|
103
|
+
await websocket.send_json({"error": str(e)})
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
return
|
|
107
|
+
else:
|
|
108
|
+
print(f"[video/stream] Reusing streamer for device {device_id}")
|
|
109
|
+
|
|
110
|
+
streamer = scrcpy_streamers[device_id]
|
|
111
|
+
if streamer.cached_sps and streamer.cached_pps:
|
|
112
|
+
init_data = streamer.cached_sps + streamer.cached_pps
|
|
113
|
+
await websocket.send_bytes(init_data)
|
|
114
|
+
print(f"[video/stream] Sent SPS/PPS for device {device_id}")
|
|
115
|
+
else:
|
|
116
|
+
print(
|
|
117
|
+
f"[video/stream] Warning: No cached SPS/PPS for device {device_id}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
streamer = scrcpy_streamers[device_id]
|
|
121
|
+
|
|
122
|
+
stream_failed = False
|
|
123
|
+
try:
|
|
124
|
+
chunk_count = 0
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
h264_chunk = await streamer.read_h264_chunk()
|
|
128
|
+
await websocket.send_bytes(h264_chunk)
|
|
129
|
+
chunk_count += 1
|
|
130
|
+
if chunk_count % 100 == 0:
|
|
131
|
+
print(
|
|
132
|
+
f"[video/stream] Device {device_id}: Sent {chunk_count} chunks"
|
|
133
|
+
)
|
|
134
|
+
except ConnectionError as e:
|
|
135
|
+
print(f"[video/stream] Device {device_id}: Connection error: {e}")
|
|
136
|
+
stream_failed = True
|
|
137
|
+
try:
|
|
138
|
+
await websocket.send_json({"error": f"Stream error: {str(e)}"})
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
except WebSocketDisconnect:
|
|
144
|
+
print(f"[video/stream] Device {device_id}: Client disconnected")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
import traceback
|
|
147
|
+
|
|
148
|
+
print(f"[video/stream] Device {device_id}: Error: {e}")
|
|
149
|
+
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
150
|
+
stream_failed = True
|
|
151
|
+
try:
|
|
152
|
+
await websocket.send_json({"error": str(e)})
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
if stream_failed:
|
|
157
|
+
async with scrcpy_locks[device_id]:
|
|
158
|
+
if device_id in scrcpy_streamers:
|
|
159
|
+
print(f"[video/stream] Resetting streamer for device {device_id}")
|
|
160
|
+
scrcpy_streamers[device_id].stop()
|
|
161
|
+
del scrcpy_streamers[device_id]
|
|
162
|
+
|
|
163
|
+
print(f"[video/stream] Device {device_id}: Stream ended")
|
AutoGLM_GUI/schemas.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Shared Pydantic models for the AutoGLM-GUI API."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class APIModelConfig(BaseModel):
|
|
7
|
+
base_url: str | None = None
|
|
8
|
+
api_key: str | None = None
|
|
9
|
+
model_name: str | None = None
|
|
10
|
+
max_tokens: int = 3000
|
|
11
|
+
temperature: float = 0.0
|
|
12
|
+
top_p: float = 0.85
|
|
13
|
+
frequency_penalty: float = 0.2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIAgentConfig(BaseModel):
|
|
17
|
+
max_steps: int = 100
|
|
18
|
+
device_id: str | None = None
|
|
19
|
+
lang: str = "cn"
|
|
20
|
+
system_prompt: str | None = None
|
|
21
|
+
verbose: bool = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InitRequest(BaseModel):
|
|
25
|
+
model: APIModelConfig | None = Field(default=None, alias="model_config")
|
|
26
|
+
agent: APIAgentConfig | None = Field(default=None, alias="agent_config")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChatRequest(BaseModel):
|
|
30
|
+
message: str
|
|
31
|
+
device_id: str # 设备 ID(必填)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ChatResponse(BaseModel):
|
|
35
|
+
result: str
|
|
36
|
+
steps: int
|
|
37
|
+
success: bool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StatusResponse(BaseModel):
|
|
41
|
+
version: str
|
|
42
|
+
initialized: bool
|
|
43
|
+
step_count: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ResetRequest(BaseModel):
|
|
47
|
+
device_id: str # 设备 ID(必填)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ScreenshotRequest(BaseModel):
|
|
51
|
+
device_id: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ScreenshotResponse(BaseModel):
|
|
55
|
+
success: bool
|
|
56
|
+
image: str # base64 encoded PNG
|
|
57
|
+
width: int
|
|
58
|
+
height: int
|
|
59
|
+
is_sensitive: bool
|
|
60
|
+
error: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TapRequest(BaseModel):
|
|
64
|
+
x: int
|
|
65
|
+
y: int
|
|
66
|
+
device_id: str | None = None
|
|
67
|
+
delay: float = 0.0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TapResponse(BaseModel):
|
|
71
|
+
success: bool
|
|
72
|
+
error: str | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SwipeRequest(BaseModel):
|
|
76
|
+
start_x: int
|
|
77
|
+
start_y: int
|
|
78
|
+
end_x: int
|
|
79
|
+
end_y: int
|
|
80
|
+
duration_ms: int | None = None
|
|
81
|
+
device_id: str | None = None
|
|
82
|
+
delay: float = 0.0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SwipeResponse(BaseModel):
|
|
86
|
+
success: bool
|
|
87
|
+
error: str | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TouchDownRequest(BaseModel):
|
|
91
|
+
x: int
|
|
92
|
+
y: int
|
|
93
|
+
device_id: str | None = None
|
|
94
|
+
delay: float = 0.0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TouchDownResponse(BaseModel):
|
|
98
|
+
success: bool
|
|
99
|
+
error: str | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TouchMoveRequest(BaseModel):
|
|
103
|
+
x: int
|
|
104
|
+
y: int
|
|
105
|
+
device_id: str | None = None
|
|
106
|
+
delay: float = 0.0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TouchMoveResponse(BaseModel):
|
|
110
|
+
success: bool
|
|
111
|
+
error: str | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TouchUpRequest(BaseModel):
|
|
115
|
+
x: int
|
|
116
|
+
y: int
|
|
117
|
+
device_id: str | None = None
|
|
118
|
+
delay: float = 0.0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TouchUpResponse(BaseModel):
|
|
122
|
+
success: bool
|
|
123
|
+
error: str | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class DeviceListResponse(BaseModel):
|
|
127
|
+
devices: list[dict]
|
AutoGLM_GUI/scrcpy_stream.py
CHANGED
|
@@ -104,6 +104,7 @@ class ScrcpyStreamer:
|
|
|
104
104
|
except Exception as e:
|
|
105
105
|
print(f"[ScrcpyStreamer] Failed to start: {e}")
|
|
106
106
|
import traceback
|
|
107
|
+
|
|
107
108
|
traceback.print_exc()
|
|
108
109
|
self.stop()
|
|
109
110
|
raise RuntimeError(f"Failed to start scrcpy server: {e}") from e
|
|
@@ -124,7 +125,7 @@ class ScrcpyStreamer:
|
|
|
124
125
|
# Method 2: Find and kill by PID (more reliable)
|
|
125
126
|
cmd = cmd_base + [
|
|
126
127
|
"shell",
|
|
127
|
-
"ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9"
|
|
128
|
+
"ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9",
|
|
128
129
|
]
|
|
129
130
|
process = await asyncio.create_subprocess_exec(
|
|
130
131
|
*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
@@ -200,9 +201,7 @@ class ScrcpyStreamer:
|
|
|
200
201
|
|
|
201
202
|
# Capture stderr to see error messages
|
|
202
203
|
self.scrcpy_process = await asyncio.create_subprocess_exec(
|
|
203
|
-
*cmd,
|
|
204
|
-
stdout=subprocess.PIPE,
|
|
205
|
-
stderr=subprocess.PIPE
|
|
204
|
+
*cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
206
205
|
)
|
|
207
206
|
|
|
208
207
|
# Wait for server to start
|
|
@@ -217,12 +216,16 @@ class ScrcpyStreamer:
|
|
|
217
216
|
# Check if it's an "Address already in use" error
|
|
218
217
|
if "Address already in use" in error_msg:
|
|
219
218
|
if attempt < max_retries - 1:
|
|
220
|
-
print(
|
|
219
|
+
print(
|
|
220
|
+
f"[ScrcpyStreamer] Address in use, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})..."
|
|
221
|
+
)
|
|
221
222
|
await self._cleanup_existing_server()
|
|
222
223
|
await asyncio.sleep(retry_delay)
|
|
223
224
|
continue
|
|
224
225
|
else:
|
|
225
|
-
raise RuntimeError(
|
|
226
|
+
raise RuntimeError(
|
|
227
|
+
f"scrcpy server failed after {max_retries} attempts: {error_msg}"
|
|
228
|
+
)
|
|
226
229
|
else:
|
|
227
230
|
raise RuntimeError(f"scrcpy server exited immediately: {error_msg}")
|
|
228
231
|
|
|
@@ -239,7 +242,9 @@ class ScrcpyStreamer:
|
|
|
239
242
|
# Increase socket buffer size for high-resolution video
|
|
240
243
|
# Default is often 64KB, but complex frames can be 200-500KB
|
|
241
244
|
try:
|
|
242
|
-
self.tcp_socket.setsockopt(
|
|
245
|
+
self.tcp_socket.setsockopt(
|
|
246
|
+
socket.SOL_SOCKET, socket.SO_RCVBUF, 2 * 1024 * 1024
|
|
247
|
+
) # 2MB
|
|
243
248
|
print("[ScrcpyStreamer] Set socket receive buffer to 2MB")
|
|
244
249
|
except OSError as e:
|
|
245
250
|
print(f"[ScrcpyStreamer] Warning: Failed to set socket buffer size: {e}")
|
|
@@ -267,9 +272,9 @@ class ScrcpyStreamer:
|
|
|
267
272
|
|
|
268
273
|
while i < data_len - 4:
|
|
269
274
|
# Look for start codes: 0x00 0x00 0x00 0x01 or 0x00 0x00 0x01
|
|
270
|
-
if data[i:i+4] == b
|
|
275
|
+
if data[i : i + 4] == b"\x00\x00\x00\x01":
|
|
271
276
|
start_code_len = 4
|
|
272
|
-
elif data[i:i+3] == b
|
|
277
|
+
elif data[i : i + 3] == b"\x00\x00\x01":
|
|
273
278
|
start_code_len = 3
|
|
274
279
|
else:
|
|
275
280
|
i += 1
|
|
@@ -285,8 +290,10 @@ class ScrcpyStreamer:
|
|
|
285
290
|
# Find next start code to determine NAL unit size
|
|
286
291
|
next_start = nal_start + 1
|
|
287
292
|
while next_start < data_len - 3:
|
|
288
|
-
if (
|
|
289
|
-
data[next_start:next_start+
|
|
293
|
+
if (
|
|
294
|
+
data[next_start : next_start + 4] == b"\x00\x00\x00\x01"
|
|
295
|
+
or data[next_start : next_start + 3] == b"\x00\x00\x01"
|
|
296
|
+
):
|
|
290
297
|
break
|
|
291
298
|
next_start += 1
|
|
292
299
|
else:
|
|
@@ -309,7 +316,7 @@ class ScrcpyStreamer:
|
|
|
309
316
|
nal_units = self._find_nal_units(data)
|
|
310
317
|
|
|
311
318
|
for start, nal_type, size in nal_units:
|
|
312
|
-
nal_data = data[start:start+size]
|
|
319
|
+
nal_data = data[start : start + size]
|
|
313
320
|
|
|
314
321
|
if nal_type == 7: # SPS
|
|
315
322
|
# Only cache SPS if not yet locked
|
|
@@ -317,10 +324,16 @@ class ScrcpyStreamer:
|
|
|
317
324
|
# Validate: SPS should be at least 10 bytes
|
|
318
325
|
if size >= 10 and not self.cached_sps:
|
|
319
326
|
self.cached_sps = nal_data
|
|
320
|
-
hex_preview =
|
|
321
|
-
|
|
327
|
+
hex_preview = " ".join(
|
|
328
|
+
f"{b:02x}" for b in nal_data[: min(12, len(nal_data))]
|
|
329
|
+
)
|
|
330
|
+
print(
|
|
331
|
+
f"[ScrcpyStreamer] ✓ Cached complete SPS ({size} bytes): {hex_preview}..."
|
|
332
|
+
)
|
|
322
333
|
elif size < 10:
|
|
323
|
-
print(
|
|
334
|
+
print(
|
|
335
|
+
f"[ScrcpyStreamer] ✗ Skipped truncated SPS ({size} bytes, too short)"
|
|
336
|
+
)
|
|
324
337
|
|
|
325
338
|
elif nal_type == 8: # PPS
|
|
326
339
|
# Only cache PPS if not yet locked
|
|
@@ -328,10 +341,16 @@ class ScrcpyStreamer:
|
|
|
328
341
|
# Validate: PPS should be at least 6 bytes
|
|
329
342
|
if size >= 6 and not self.cached_pps:
|
|
330
343
|
self.cached_pps = nal_data
|
|
331
|
-
hex_preview =
|
|
332
|
-
|
|
344
|
+
hex_preview = " ".join(
|
|
345
|
+
f"{b:02x}" for b in nal_data[: min(12, len(nal_data))]
|
|
346
|
+
)
|
|
347
|
+
print(
|
|
348
|
+
f"[ScrcpyStreamer] ✓ Cached complete PPS ({size} bytes): {hex_preview}..."
|
|
349
|
+
)
|
|
333
350
|
elif size < 6:
|
|
334
|
-
print(
|
|
351
|
+
print(
|
|
352
|
+
f"[ScrcpyStreamer] ✗ Skipped truncated PPS ({size} bytes, too short)"
|
|
353
|
+
)
|
|
335
354
|
|
|
336
355
|
elif nal_type == 5: # IDR frame
|
|
337
356
|
# ✅ ALWAYS update IDR to keep the LATEST frame
|
|
@@ -340,7 +359,9 @@ class ScrcpyStreamer:
|
|
|
340
359
|
is_first = self.cached_idr is None
|
|
341
360
|
self.cached_idr = nal_data
|
|
342
361
|
if is_first:
|
|
343
|
-
print(
|
|
362
|
+
print(
|
|
363
|
+
f"[ScrcpyStreamer] ✓ Cached initial IDR frame ({size} bytes)"
|
|
364
|
+
)
|
|
344
365
|
# Don't log every IDR update (too verbose)
|
|
345
366
|
|
|
346
367
|
# Lock SPS/PPS once we have complete initial parameters
|
|
@@ -366,7 +387,9 @@ class ScrcpyStreamer:
|
|
|
366
387
|
return data
|
|
367
388
|
|
|
368
389
|
# Find all IDR frames
|
|
369
|
-
idr_positions = [
|
|
390
|
+
idr_positions = [
|
|
391
|
+
(start, size) for start, nal_type, size in nal_units if nal_type == 5
|
|
392
|
+
]
|
|
370
393
|
|
|
371
394
|
if not idr_positions:
|
|
372
395
|
return data
|
|
@@ -386,7 +409,9 @@ class ScrcpyStreamer:
|
|
|
386
409
|
if data[prepend_offset:idr_start] != sps_pps:
|
|
387
410
|
# Prepend SPS/PPS before this IDR
|
|
388
411
|
result.extend(sps_pps)
|
|
389
|
-
print(
|
|
412
|
+
print(
|
|
413
|
+
f"[ScrcpyStreamer] Prepended SPS/PPS before IDR at position {idr_start}"
|
|
414
|
+
)
|
|
390
415
|
|
|
391
416
|
# Update position to start of IDR
|
|
392
417
|
last_pos = idr_start
|
|
@@ -409,11 +434,17 @@ class ScrcpyStreamer:
|
|
|
409
434
|
init_data += self.cached_idr
|
|
410
435
|
|
|
411
436
|
# Validate data integrity
|
|
412
|
-
print(
|
|
413
|
-
print(
|
|
414
|
-
|
|
437
|
+
print("[ScrcpyStreamer] Returning init data:")
|
|
438
|
+
print(
|
|
439
|
+
f" - SPS: {len(self.cached_sps)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_sps[:8])}"
|
|
440
|
+
)
|
|
441
|
+
print(
|
|
442
|
+
f" - PPS: {len(self.cached_pps)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_pps[:8])}"
|
|
443
|
+
)
|
|
415
444
|
if self.cached_idr:
|
|
416
|
-
print(
|
|
445
|
+
print(
|
|
446
|
+
f" - IDR: {len(self.cached_idr)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_idr[:8])}"
|
|
447
|
+
)
|
|
417
448
|
print(f" - Total: {len(init_data)} bytes")
|
|
418
449
|
|
|
419
450
|
return init_data
|
|
@@ -442,7 +473,9 @@ class ScrcpyStreamer:
|
|
|
442
473
|
|
|
443
474
|
# Log large chunks (might indicate complex frames)
|
|
444
475
|
if len(data) > 200 * 1024: # > 200KB
|
|
445
|
-
print(
|
|
476
|
+
print(
|
|
477
|
+
f"[ScrcpyStreamer] Large chunk received: {len(data) / 1024:.1f} KB"
|
|
478
|
+
)
|
|
446
479
|
|
|
447
480
|
# Cache INITIAL complete SPS/PPS/IDR for future use
|
|
448
481
|
# (Later chunks may have truncated NAL units, so we only cache once)
|
|
@@ -457,7 +490,9 @@ class ScrcpyStreamer:
|
|
|
457
490
|
except ConnectionError:
|
|
458
491
|
raise
|
|
459
492
|
except Exception as e:
|
|
460
|
-
print(
|
|
493
|
+
print(
|
|
494
|
+
f"[ScrcpyStreamer] Unexpected error in read_h264_chunk: {type(e).__name__}: {e}"
|
|
495
|
+
)
|
|
461
496
|
raise ConnectionError(f"Failed to read from socket: {e}") from e
|
|
462
497
|
|
|
463
498
|
def stop(self) -> None:
|
|
@@ -489,7 +524,9 @@ class ScrcpyStreamer:
|
|
|
489
524
|
if self.device_id:
|
|
490
525
|
cmd.extend(["-s", self.device_id])
|
|
491
526
|
cmd.extend(["forward", "--remove", f"tcp:{self.port}"])
|
|
492
|
-
subprocess.run(
|
|
527
|
+
subprocess.run(
|
|
528
|
+
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2
|
|
529
|
+
)
|
|
493
530
|
except Exception:
|
|
494
531
|
pass
|
|
495
532
|
self.forward_cleanup_needed = False
|