autoglm-gui 0.4.6__tar.gz → 0.4.8__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.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/media.py +127 -13
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/scrcpy_stream.py +123 -72
- autoglm_gui-0.4.6/AutoGLM_GUI/static/assets/about-BgmYMjz1.js → autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/about-BI6OV6gm.js +1 -1
- autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/chat-C_2Cot0q.js +25 -0
- autoglm_gui-0.4.6/AutoGLM_GUI/static/assets/index-sfTWLHvP.js → autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/index-Dn3vR6uV.js +1 -1
- autoglm_gui-0.4.6/AutoGLM_GUI/static/assets/index-Dpft8C1z.js → autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/index-Do7ha9Kf.js +1 -1
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/static/index.html +1 -1
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/PKG-INFO +1 -1
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/pyproject.toml +1 -1
- autoglm_gui-0.4.6/AutoGLM_GUI/static/assets/chat-CnjZadB7.js +0 -25
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/.gitignore +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/__main__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/adb_plus/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/adb_plus/screenshot.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/adb_plus/touch.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/agents.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/control.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/devices.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/config.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/schemas.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/server.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/state.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/static/assets/index-DCrxTz-A.css +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/version.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/LICENSE +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/README.md +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/actions/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/actions/handler.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/connection.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/device.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/input.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/screenshot.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/agent.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/apps.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/i18n.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/prompts.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/prompts_en.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/prompts_zh.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/model/__init__.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/model/client.py +0 -0
- {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/scrcpy-server-v3.3.3 +0 -0
|
@@ -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:
|
|
@@ -68,7 +73,10 @@ def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
|
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
@router.websocket("/api/video/stream")
|
|
71
|
-
async def video_stream_ws(
|
|
76
|
+
async def video_stream_ws(
|
|
77
|
+
websocket: WebSocket,
|
|
78
|
+
device_id: str | None = None,
|
|
79
|
+
):
|
|
72
80
|
"""Stream real-time H.264 video from scrcpy server via WebSocket(多设备支持)."""
|
|
73
81
|
await websocket.accept()
|
|
74
82
|
|
|
@@ -78,6 +86,15 @@ async def video_stream_ws(websocket: WebSocket, device_id: str | None = None):
|
|
|
78
86
|
|
|
79
87
|
print(f"[video/stream] WebSocket connection for device {device_id}")
|
|
80
88
|
|
|
89
|
+
# Debug: Save stream to file for analysis (controlled by DEBUG_SAVE_VIDEO_STREAM env var)
|
|
90
|
+
debug_file = None
|
|
91
|
+
if DEBUG_SAVE_STREAM:
|
|
92
|
+
debug_dir = Path("debug_streams")
|
|
93
|
+
debug_dir.mkdir(exist_ok=True)
|
|
94
|
+
debug_file_path = debug_dir / f"{device_id}_{int(__import__('time').time())}.h264"
|
|
95
|
+
debug_file = open(debug_file_path, "wb")
|
|
96
|
+
print(f"[video/stream] DEBUG: Saving stream to {debug_file_path}")
|
|
97
|
+
|
|
81
98
|
if device_id not in scrcpy_locks:
|
|
82
99
|
scrcpy_locks[device_id] = asyncio.Lock()
|
|
83
100
|
|
|
@@ -92,6 +109,53 @@ async def video_stream_ws(websocket: WebSocket, device_id: str | None = None):
|
|
|
92
109
|
print(f"[video/stream] Starting scrcpy server for device {device_id}")
|
|
93
110
|
await scrcpy_streamers[device_id].start()
|
|
94
111
|
print(f"[video/stream] Scrcpy server started for device {device_id}")
|
|
112
|
+
|
|
113
|
+
# Read NAL units until we have SPS, PPS, and IDR
|
|
114
|
+
streamer = scrcpy_streamers[device_id]
|
|
115
|
+
|
|
116
|
+
print("[video/stream] Reading NAL units for initialization...")
|
|
117
|
+
for attempt in range(20): # Max 20 NAL units for initialization
|
|
118
|
+
try:
|
|
119
|
+
nal_unit = await streamer.read_nal_unit(auto_cache=True)
|
|
120
|
+
nal_type = nal_unit[4] & 0x1F if len(nal_unit) > 4 else -1
|
|
121
|
+
nal_type_names = {5: "IDR", 7: "SPS", 8: "PPS"}
|
|
122
|
+
print(
|
|
123
|
+
f"[video/stream] Read NAL unit: type={nal_type_names.get(nal_type, nal_type)}, size={len(nal_unit)} bytes"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Check if we have all required parameter sets
|
|
127
|
+
if (
|
|
128
|
+
streamer.cached_sps
|
|
129
|
+
and streamer.cached_pps
|
|
130
|
+
and streamer.cached_idr
|
|
131
|
+
):
|
|
132
|
+
print(
|
|
133
|
+
f"[video/stream] ✓ Initialization complete: SPS={len(streamer.cached_sps)}B, PPS={len(streamer.cached_pps)}B, IDR={len(streamer.cached_idr)}B"
|
|
134
|
+
)
|
|
135
|
+
break
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"[video/stream] Failed to read NAL unit: {e}")
|
|
138
|
+
await asyncio.sleep(0.5)
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Get initialization data (SPS + PPS + IDR)
|
|
142
|
+
init_data = streamer.get_initialization_data()
|
|
143
|
+
if not init_data:
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
"Failed to get initialization data (missing SPS/PPS/IDR)"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Send initialization data as ONE message (SPS+PPS+IDR combined)
|
|
149
|
+
await websocket.send_bytes(init_data)
|
|
150
|
+
print(
|
|
151
|
+
f"[video/stream] ✓ Sent initialization data to first client: {len(init_data)} bytes total"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Debug: Save to file
|
|
155
|
+
if debug_file:
|
|
156
|
+
debug_file.write(init_data)
|
|
157
|
+
debug_file.flush()
|
|
158
|
+
|
|
95
159
|
except Exception as e:
|
|
96
160
|
import traceback
|
|
97
161
|
|
|
@@ -108,28 +172,73 @@ async def video_stream_ws(websocket: WebSocket, device_id: str | None = None):
|
|
|
108
172
|
print(f"[video/stream] Reusing streamer for device {device_id}")
|
|
109
173
|
|
|
110
174
|
streamer = scrcpy_streamers[device_id]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
175
|
+
# CRITICAL: Send complete initialization data (SPS+PPS+IDR)
|
|
176
|
+
# Without IDR frame, decoder cannot start and will show black screen
|
|
177
|
+
|
|
178
|
+
# Wait for initialization data to be ready (max 5 seconds)
|
|
179
|
+
init_data = None
|
|
180
|
+
for attempt in range(10):
|
|
181
|
+
init_data = streamer.get_initialization_data()
|
|
182
|
+
if init_data:
|
|
183
|
+
break
|
|
184
|
+
print(
|
|
185
|
+
f"[video/stream] Waiting for initialization data (attempt {attempt + 1}/10)..."
|
|
186
|
+
)
|
|
187
|
+
await asyncio.sleep(0.5)
|
|
188
|
+
|
|
189
|
+
if init_data:
|
|
190
|
+
# Log what we're sending
|
|
191
|
+
print(
|
|
192
|
+
f"[video/stream] ✓ Sending cached initialization data for device {device_id}:"
|
|
193
|
+
)
|
|
194
|
+
print(
|
|
195
|
+
f" - SPS: {len(streamer.cached_sps) if streamer.cached_sps else 0}B"
|
|
196
|
+
)
|
|
116
197
|
print(
|
|
117
|
-
f"
|
|
198
|
+
f" - PPS: {len(streamer.cached_pps) if streamer.cached_pps else 0}B"
|
|
118
199
|
)
|
|
200
|
+
print(
|
|
201
|
+
f" - IDR: {len(streamer.cached_idr) if streamer.cached_idr else 0}B"
|
|
202
|
+
)
|
|
203
|
+
print(f" - Total: {len(init_data)} bytes")
|
|
204
|
+
|
|
205
|
+
await websocket.send_bytes(init_data)
|
|
206
|
+
print("[video/stream] ✓ Initialization data sent successfully")
|
|
207
|
+
|
|
208
|
+
# Debug: Save to file
|
|
209
|
+
if debug_file:
|
|
210
|
+
debug_file.write(init_data)
|
|
211
|
+
debug_file.flush()
|
|
212
|
+
else:
|
|
213
|
+
error_msg = f"Initialization data not ready for device {device_id} after 5 seconds"
|
|
214
|
+
print(f"[video/stream] ERROR: {error_msg}")
|
|
215
|
+
try:
|
|
216
|
+
await websocket.send_json({"error": error_msg})
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
return
|
|
119
220
|
|
|
120
221
|
streamer = scrcpy_streamers[device_id]
|
|
121
222
|
|
|
122
223
|
stream_failed = False
|
|
123
224
|
try:
|
|
124
|
-
|
|
225
|
+
nal_count = 0
|
|
125
226
|
while True:
|
|
126
227
|
try:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
228
|
+
# Read one complete NAL unit
|
|
229
|
+
# Each WebSocket message = one complete NAL unit (clear semantic boundary)
|
|
230
|
+
nal_unit = await streamer.read_nal_unit(auto_cache=True)
|
|
231
|
+
await websocket.send_bytes(nal_unit)
|
|
232
|
+
|
|
233
|
+
# Debug: Save to file
|
|
234
|
+
if debug_file:
|
|
235
|
+
debug_file.write(nal_unit)
|
|
236
|
+
debug_file.flush()
|
|
237
|
+
|
|
238
|
+
nal_count += 1
|
|
239
|
+
if nal_count % 100 == 0:
|
|
131
240
|
print(
|
|
132
|
-
f"[video/stream] Device {device_id}: Sent {
|
|
241
|
+
f"[video/stream] Device {device_id}: Sent {nal_count} NAL units"
|
|
133
242
|
)
|
|
134
243
|
except ConnectionError as e:
|
|
135
244
|
print(f"[video/stream] Device {device_id}: Connection error: {e}")
|
|
@@ -160,4 +269,9 @@ async def video_stream_ws(websocket: WebSocket, device_id: str | None = None):
|
|
|
160
269
|
scrcpy_streamers[device_id].stop()
|
|
161
270
|
del scrcpy_streamers[device_id]
|
|
162
271
|
|
|
272
|
+
# Debug: Close file
|
|
273
|
+
if debug_file:
|
|
274
|
+
debug_file.close()
|
|
275
|
+
print("[video/stream] DEBUG: Closed debug file")
|
|
276
|
+
|
|
163
277
|
print(f"[video/stream] Device {device_id}: Stream ended")
|
|
@@ -47,6 +47,9 @@ class ScrcpyStreamer:
|
|
|
47
47
|
self.sps_pps_locked = False # Lock SPS/PPS after initial complete capture
|
|
48
48
|
# Note: IDR is NOT locked - we keep updating to the latest frame
|
|
49
49
|
|
|
50
|
+
# NAL unit reading buffer (for read_nal_unit method)
|
|
51
|
+
self._nal_read_buffer = bytearray()
|
|
52
|
+
|
|
50
53
|
# Find scrcpy-server location
|
|
51
54
|
self.scrcpy_server_path = self._find_scrcpy_server()
|
|
52
55
|
|
|
@@ -83,6 +86,10 @@ class ScrcpyStreamer:
|
|
|
83
86
|
|
|
84
87
|
async def start(self) -> None:
|
|
85
88
|
"""Start scrcpy server and establish connection."""
|
|
89
|
+
# Clear NAL reading buffer to ensure clean state
|
|
90
|
+
self._nal_read_buffer.clear()
|
|
91
|
+
print("[ScrcpyStreamer] Cleared NAL read buffer")
|
|
92
|
+
|
|
86
93
|
try:
|
|
87
94
|
# 0. Kill existing scrcpy server processes on device
|
|
88
95
|
print("[ScrcpyStreamer] Cleaning up existing scrcpy processes...")
|
|
@@ -307,11 +314,14 @@ class ScrcpyStreamer:
|
|
|
307
314
|
|
|
308
315
|
raise ConnectionError("Failed to connect to scrcpy server")
|
|
309
316
|
|
|
310
|
-
def _find_nal_units(
|
|
317
|
+
def _find_nal_units(
|
|
318
|
+
self, data: bytes
|
|
319
|
+
) -> list[tuple[int, int, int, bool]]:
|
|
311
320
|
"""Find NAL units in H.264 data.
|
|
312
321
|
|
|
313
322
|
Returns:
|
|
314
|
-
List of (start_pos, nal_type, nal_size) tuples
|
|
323
|
+
List of (start_pos, nal_type, nal_size, is_complete) tuples
|
|
324
|
+
is_complete=False if NAL unit extends to chunk boundary (may be truncated)
|
|
315
325
|
"""
|
|
316
326
|
nal_units = []
|
|
317
327
|
i = 0
|
|
@@ -336,18 +346,22 @@ class ScrcpyStreamer:
|
|
|
336
346
|
|
|
337
347
|
# Find next start code to determine NAL unit size
|
|
338
348
|
next_start = nal_start + 1
|
|
349
|
+
found_next = False
|
|
339
350
|
while next_start < data_len - 3:
|
|
340
351
|
if (
|
|
341
352
|
data[next_start : next_start + 4] == b"\x00\x00\x00\x01"
|
|
342
353
|
or data[next_start : next_start + 3] == b"\x00\x00\x01"
|
|
343
354
|
):
|
|
355
|
+
found_next = True
|
|
344
356
|
break
|
|
345
357
|
next_start += 1
|
|
346
358
|
else:
|
|
347
359
|
next_start = data_len
|
|
348
360
|
|
|
349
361
|
nal_size = next_start - i
|
|
350
|
-
|
|
362
|
+
# NAL unit is complete only if we found the next start code
|
|
363
|
+
is_complete = found_next
|
|
364
|
+
nal_units.append((i, nal_type, nal_size, is_complete))
|
|
351
365
|
|
|
352
366
|
i = next_start
|
|
353
367
|
|
|
@@ -356,13 +370,13 @@ class ScrcpyStreamer:
|
|
|
356
370
|
def _cache_nal_units(self, data: bytes) -> None:
|
|
357
371
|
"""Parse and cache INITIAL complete NAL units (SPS, PPS, IDR).
|
|
358
372
|
|
|
359
|
-
IMPORTANT:
|
|
360
|
-
NAL units
|
|
361
|
-
|
|
373
|
+
IMPORTANT: Caches NAL units with size validation.
|
|
374
|
+
For small NAL units (SPS/PPS), we cache even if at chunk boundary.
|
|
375
|
+
For large NAL units (IDR), we require minimum size to ensure completeness.
|
|
362
376
|
"""
|
|
363
377
|
nal_units = self._find_nal_units(data)
|
|
364
378
|
|
|
365
|
-
for start, nal_type, size in nal_units:
|
|
379
|
+
for start, nal_type, size, is_complete in nal_units:
|
|
366
380
|
nal_data = data[start : start + size]
|
|
367
381
|
|
|
368
382
|
if nal_type == 7: # SPS
|
|
@@ -375,11 +389,11 @@ class ScrcpyStreamer:
|
|
|
375
389
|
f"{b:02x}" for b in nal_data[: min(12, len(nal_data))]
|
|
376
390
|
)
|
|
377
391
|
print(
|
|
378
|
-
f"[ScrcpyStreamer] ✓ Cached
|
|
392
|
+
f"[ScrcpyStreamer] ✓ Cached SPS ({size} bytes, complete={is_complete}): {hex_preview}..."
|
|
379
393
|
)
|
|
380
394
|
elif size < 10:
|
|
381
395
|
print(
|
|
382
|
-
f"[ScrcpyStreamer] ✗ Skipped
|
|
396
|
+
f"[ScrcpyStreamer] ✗ Skipped short SPS ({size} bytes)"
|
|
383
397
|
)
|
|
384
398
|
|
|
385
399
|
elif nal_type == 8: # PPS
|
|
@@ -392,82 +406,36 @@ class ScrcpyStreamer:
|
|
|
392
406
|
f"{b:02x}" for b in nal_data[: min(12, len(nal_data))]
|
|
393
407
|
)
|
|
394
408
|
print(
|
|
395
|
-
f"[ScrcpyStreamer] ✓ Cached
|
|
409
|
+
f"[ScrcpyStreamer] ✓ Cached PPS ({size} bytes, complete={is_complete}): {hex_preview}..."
|
|
396
410
|
)
|
|
397
411
|
elif size < 6:
|
|
398
412
|
print(
|
|
399
|
-
f"[ScrcpyStreamer] ✗ Skipped
|
|
413
|
+
f"[ScrcpyStreamer] ✗ Skipped short PPS ({size} bytes)"
|
|
400
414
|
)
|
|
401
415
|
|
|
402
416
|
elif nal_type == 5: # IDR frame
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
|
|
417
|
+
# Cache IDR if it's large enough (size check is sufficient)
|
|
418
|
+
# Note: When called from read_nal_unit(), the NAL is guaranteed complete
|
|
419
|
+
# because we extract it between two start codes. The is_complete flag
|
|
420
|
+
# is only False because the NAL is isolated (no next start code in buffer).
|
|
421
|
+
if self.cached_sps and self.cached_pps and size >= 1024:
|
|
406
422
|
is_first = self.cached_idr is None
|
|
407
423
|
self.cached_idr = nal_data
|
|
408
424
|
if is_first:
|
|
409
425
|
print(
|
|
410
|
-
f"[ScrcpyStreamer] ✓ Cached
|
|
426
|
+
f"[ScrcpyStreamer] ✓ Cached IDR frame ({size} bytes)"
|
|
411
427
|
)
|
|
412
428
|
# Don't log every IDR update (too verbose)
|
|
429
|
+
elif size < 1024:
|
|
430
|
+
print(
|
|
431
|
+
f"[ScrcpyStreamer] ✗ Skipped small IDR ({size} bytes, likely incomplete)"
|
|
432
|
+
)
|
|
413
433
|
|
|
414
434
|
# Lock SPS/PPS once we have complete initial parameters
|
|
415
435
|
if self.cached_sps and self.cached_pps and not self.sps_pps_locked:
|
|
416
436
|
self.sps_pps_locked = True
|
|
417
437
|
print("[ScrcpyStreamer] 🔒 SPS/PPS locked (IDR will continue updating)")
|
|
418
438
|
|
|
419
|
-
def _prepend_sps_pps_to_idr(self, data: bytes) -> bytes:
|
|
420
|
-
"""Prepend SPS/PPS before EVERY IDR frame unconditionally.
|
|
421
|
-
|
|
422
|
-
This ensures that clients can start decoding from any IDR frame,
|
|
423
|
-
even if they join mid-stream. We always prepend to guarantee
|
|
424
|
-
that every IDR is self-contained.
|
|
425
|
-
|
|
426
|
-
Returns:
|
|
427
|
-
Modified data with SPS/PPS prepended to all IDR frames
|
|
428
|
-
"""
|
|
429
|
-
if not self.cached_sps or not self.cached_pps:
|
|
430
|
-
return data
|
|
431
|
-
|
|
432
|
-
nal_units = self._find_nal_units(data)
|
|
433
|
-
if not nal_units:
|
|
434
|
-
return data
|
|
435
|
-
|
|
436
|
-
# Find all IDR frames
|
|
437
|
-
idr_positions = [
|
|
438
|
-
(start, size) for start, nal_type, size in nal_units if nal_type == 5
|
|
439
|
-
]
|
|
440
|
-
|
|
441
|
-
if not idr_positions:
|
|
442
|
-
return data
|
|
443
|
-
|
|
444
|
-
# Build modified data by prepending SPS/PPS before each IDR
|
|
445
|
-
result = bytearray()
|
|
446
|
-
last_pos = 0
|
|
447
|
-
sps_pps = self.cached_sps + self.cached_pps
|
|
448
|
-
|
|
449
|
-
for idr_start, idr_size in idr_positions:
|
|
450
|
-
# Add data before this IDR
|
|
451
|
-
result.extend(data[last_pos:idr_start])
|
|
452
|
-
|
|
453
|
-
# Check if SPS/PPS already exists right before this IDR
|
|
454
|
-
# (to avoid duplicating if scrcpy already sent them)
|
|
455
|
-
prepend_offset = max(0, idr_start - len(sps_pps))
|
|
456
|
-
if data[prepend_offset:idr_start] != sps_pps:
|
|
457
|
-
# Prepend SPS/PPS before this IDR
|
|
458
|
-
result.extend(sps_pps)
|
|
459
|
-
print(
|
|
460
|
-
f"[ScrcpyStreamer] Prepended SPS/PPS before IDR at position {idr_start}"
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
# Update position to start of IDR
|
|
464
|
-
last_pos = idr_start
|
|
465
|
-
|
|
466
|
-
# Add remaining data (including all IDR frames and data after)
|
|
467
|
-
result.extend(data[last_pos:])
|
|
468
|
-
|
|
469
|
-
return bytes(result)
|
|
470
|
-
|
|
471
439
|
def get_initialization_data(self) -> bytes | None:
|
|
472
440
|
"""Get cached SPS/PPS/IDR for initializing new connections.
|
|
473
441
|
|
|
@@ -497,11 +465,14 @@ class ScrcpyStreamer:
|
|
|
497
465
|
return init_data
|
|
498
466
|
return None
|
|
499
467
|
|
|
500
|
-
async def read_h264_chunk(self) -> bytes:
|
|
468
|
+
async def read_h264_chunk(self, auto_cache: bool = True) -> bytes:
|
|
501
469
|
"""Read H.264 data chunk from socket.
|
|
502
470
|
|
|
471
|
+
Args:
|
|
472
|
+
auto_cache: If True, automatically cache SPS/PPS/IDR from this chunk
|
|
473
|
+
|
|
503
474
|
Returns:
|
|
504
|
-
bytes: Raw H.264 data
|
|
475
|
+
bytes: Raw H.264 data
|
|
505
476
|
|
|
506
477
|
Raises:
|
|
507
478
|
ConnectionError: If socket is closed or error occurs
|
|
@@ -524,9 +495,9 @@ class ScrcpyStreamer:
|
|
|
524
495
|
f"[ScrcpyStreamer] Large chunk received: {len(data) / 1024:.1f} KB"
|
|
525
496
|
)
|
|
526
497
|
|
|
527
|
-
#
|
|
528
|
-
|
|
529
|
-
|
|
498
|
+
# Optionally cache SPS/PPS/IDR from this chunk
|
|
499
|
+
if auto_cache:
|
|
500
|
+
self._cache_nal_units(data)
|
|
530
501
|
|
|
531
502
|
# NOTE: We don't automatically prepend SPS/PPS here because:
|
|
532
503
|
# 1. NAL units may be truncated across chunks
|
|
@@ -542,6 +513,86 @@ class ScrcpyStreamer:
|
|
|
542
513
|
)
|
|
543
514
|
raise ConnectionError(f"Failed to read from socket: {e}") from e
|
|
544
515
|
|
|
516
|
+
async def read_nal_unit(self, auto_cache: bool = True) -> bytes:
|
|
517
|
+
"""Read one complete NAL unit from socket.
|
|
518
|
+
|
|
519
|
+
This method ensures each returned chunk is a complete, self-contained NAL unit.
|
|
520
|
+
WebSocket messages will have clear semantic boundaries (one message = one NAL unit).
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
auto_cache: If True, automatically cache SPS/PPS/IDR from this NAL unit
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
bytes: Complete NAL unit (including start code)
|
|
527
|
+
|
|
528
|
+
Raises:
|
|
529
|
+
ConnectionError: If socket is closed or error occurs
|
|
530
|
+
"""
|
|
531
|
+
if not self.tcp_socket:
|
|
532
|
+
raise ConnectionError("Socket not connected")
|
|
533
|
+
|
|
534
|
+
while True:
|
|
535
|
+
# Look for start codes in buffer
|
|
536
|
+
buffer = bytes(self._nal_read_buffer)
|
|
537
|
+
start_positions = []
|
|
538
|
+
|
|
539
|
+
# Find all start codes (0x00 0x00 0x00 0x01 or 0x00 0x00 0x01)
|
|
540
|
+
i = 0
|
|
541
|
+
while i < len(buffer) - 3:
|
|
542
|
+
if buffer[i] == 0x00 and buffer[i + 1] == 0x00:
|
|
543
|
+
if buffer[i + 2] == 0x00 and buffer[i + 3] == 0x01:
|
|
544
|
+
start_positions.append(i)
|
|
545
|
+
i += 4
|
|
546
|
+
elif buffer[i + 2] == 0x01:
|
|
547
|
+
start_positions.append(i)
|
|
548
|
+
i += 3
|
|
549
|
+
else:
|
|
550
|
+
i += 1
|
|
551
|
+
else:
|
|
552
|
+
i += 1
|
|
553
|
+
|
|
554
|
+
# If we have at least 2 start codes, we can extract the first NAL unit
|
|
555
|
+
if len(start_positions) >= 2:
|
|
556
|
+
# Extract first complete NAL unit (from first start code to second start code)
|
|
557
|
+
nal_unit = buffer[start_positions[0] : start_positions[1]]
|
|
558
|
+
|
|
559
|
+
# Remove extracted NAL unit from buffer
|
|
560
|
+
self._nal_read_buffer = bytearray(buffer[start_positions[1] :])
|
|
561
|
+
|
|
562
|
+
# Cache parameter sets if enabled
|
|
563
|
+
if auto_cache:
|
|
564
|
+
self._cache_nal_units(nal_unit)
|
|
565
|
+
|
|
566
|
+
return nal_unit
|
|
567
|
+
|
|
568
|
+
# Need more data - read from socket
|
|
569
|
+
try:
|
|
570
|
+
loop = asyncio.get_event_loop()
|
|
571
|
+
chunk = await loop.run_in_executor(
|
|
572
|
+
None, self.tcp_socket.recv, 512 * 1024
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
if not chunk:
|
|
576
|
+
# Socket closed - return any remaining buffered data as final NAL unit
|
|
577
|
+
if len(self._nal_read_buffer) > 0:
|
|
578
|
+
final_nal = bytes(self._nal_read_buffer)
|
|
579
|
+
self._nal_read_buffer.clear()
|
|
580
|
+
if auto_cache:
|
|
581
|
+
self._cache_nal_units(final_nal)
|
|
582
|
+
return final_nal
|
|
583
|
+
raise ConnectionError("Socket closed by remote")
|
|
584
|
+
|
|
585
|
+
# Append new data to buffer
|
|
586
|
+
self._nal_read_buffer.extend(chunk)
|
|
587
|
+
|
|
588
|
+
except ConnectionError:
|
|
589
|
+
raise
|
|
590
|
+
except Exception as e:
|
|
591
|
+
print(
|
|
592
|
+
f"[ScrcpyStreamer] Unexpected error in read_nal_unit: {type(e).__name__}: {e}"
|
|
593
|
+
)
|
|
594
|
+
raise ConnectionError(f"Failed to read from socket: {e}") from e
|
|
595
|
+
|
|
545
596
|
def stop(self) -> None:
|
|
546
597
|
"""Stop scrcpy server and cleanup resources."""
|
|
547
598
|
# 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};
|