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.
Files changed (46) hide show
  1. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/media.py +127 -13
  2. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/scrcpy_stream.py +123 -72
  3. 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
  4. autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/chat-C_2Cot0q.js +25 -0
  5. 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
  6. 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
  7. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/static/index.html +1 -1
  8. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/PKG-INFO +1 -1
  9. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/pyproject.toml +1 -1
  10. autoglm_gui-0.4.6/AutoGLM_GUI/static/assets/chat-CnjZadB7.js +0 -25
  11. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/.gitignore +0 -0
  12. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/__init__.py +0 -0
  13. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/__main__.py +0 -0
  14. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/adb_plus/__init__.py +0 -0
  15. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/adb_plus/screenshot.py +0 -0
  16. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/adb_plus/touch.py +0 -0
  17. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/__init__.py +0 -0
  18. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/agents.py +0 -0
  19. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/control.py +0 -0
  20. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/api/devices.py +0 -0
  21. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/config.py +0 -0
  22. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/schemas.py +0 -0
  23. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/server.py +0 -0
  24. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/state.py +0 -0
  25. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/static/assets/index-DCrxTz-A.css +0 -0
  26. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/AutoGLM_GUI/version.py +0 -0
  27. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/LICENSE +0 -0
  28. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/README.md +0 -0
  29. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/__init__.py +0 -0
  30. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/actions/__init__.py +0 -0
  31. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/actions/handler.py +0 -0
  32. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/__init__.py +0 -0
  33. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/connection.py +0 -0
  34. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/device.py +0 -0
  35. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/input.py +0 -0
  36. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/adb/screenshot.py +0 -0
  37. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/agent.py +0 -0
  38. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/__init__.py +0 -0
  39. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/apps.py +0 -0
  40. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/i18n.py +0 -0
  41. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/prompts.py +0 -0
  42. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/prompts_en.py +0 -0
  43. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/config/prompts_zh.py +0 -0
  44. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/model/__init__.py +0 -0
  45. {autoglm_gui-0.4.6 → autoglm_gui-0.4.8}/phone_agent/model/client.py +0 -0
  46. {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(websocket: WebSocket, device_id: str | None = None):
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
- 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:
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"[video/stream] Warning: No cached SPS/PPS for device {device_id}"
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
- chunk_count = 0
225
+ nal_count = 0
125
226
  while True:
126
227
  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:
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 {chunk_count} chunks"
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(self, data: bytes) -> list[tuple[int, int, int]]:
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
- nal_units.append((i, nal_type, nal_size))
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: Only caches complete SPS/PPS from stream start.
360
- NAL units may be truncated across chunks, so we validate minimum sizes
361
- and lock the cache after getting complete initial parameters.
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 complete SPS ({size} bytes): {hex_preview}..."
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 truncated SPS ({size} bytes, too short)"
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 complete PPS ({size} bytes): {hex_preview}..."
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 truncated PPS ({size} bytes, too short)"
413
+ f"[ScrcpyStreamer] ✗ Skipped short PPS ({size} bytes)"
400
414
  )
401
415
 
402
416
  elif nal_type == 5: # IDR frame
403
- # ALWAYS update IDR to keep the LATEST frame
404
- # This gives better UX on reconnect (recent content, not stale startup frame)
405
- if self.cached_sps and self.cached_pps:
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 initial IDR frame ({size} bytes)"
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 with SPS/PPS prepended to IDR frames
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
- # Cache INITIAL complete SPS/PPS/IDR for future use
528
- # (Later chunks may have truncated NAL units, so we only cache once)
529
- self._cache_nal_units(data)
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-sfTWLHvP.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
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};