Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
#
|
| 3 |
import pygame
|
|
|
|
| 4 |
import time
|
| 5 |
import threading
|
| 6 |
import base64
|
|
@@ -26,39 +27,58 @@ shared = {
|
|
| 26 |
"capture_fps": 0,
|
| 27 |
"streaming": True,
|
| 28 |
"last_frame_time": time.time(),
|
| 29 |
-
"frames_this_second": 0
|
| 30 |
-
"debug_info": ""
|
| 31 |
}
|
| 32 |
|
| 33 |
# ===== 2. CREATE FLASK APP =====
|
| 34 |
app = Flask(__name__)
|
| 35 |
|
| 36 |
-
# ===== 3. HTML TEMPLATE
|
| 37 |
HTML = '''
|
| 38 |
<!DOCTYPE html>
|
| 39 |
<html>
|
| 40 |
<head>
|
| 41 |
<meta charset="UTF-8">
|
| 42 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 43 |
-
<title>PyGame +
|
| 44 |
<style>
|
| 45 |
body { font-family: Arial; background: #0f172a; color: white; padding: 20px; margin: 0; }
|
| 46 |
.container { max-width: 800px; margin: 0 auto; text-align: center; }
|
| 47 |
h1 { color: #60a5fa; }
|
| 48 |
-
.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
.stat-value { font-weight: bold; color: #60a5fa; }
|
|
|
|
| 54 |
</style>
|
| 55 |
</head>
|
| 56 |
<body>
|
| 57 |
<div class="container">
|
| 58 |
-
<h1>PyGame +
|
|
|
|
| 59 |
|
| 60 |
<div class="image-container">
|
| 61 |
-
<img id="streamImg" src="/stream" alt="Live Stream"
|
| 62 |
</div>
|
| 63 |
|
| 64 |
<div class="stats">
|
|
@@ -68,16 +88,8 @@ HTML = '''
|
|
| 68 |
<div class="stat-row"><span>Latency:</span><span class="stat-value" id="latency">-- ms</span></div>
|
| 69 |
</div>
|
| 70 |
|
| 71 |
-
<div class="
|
| 72 |
-
|
| 73 |
-
<div id="debugInfo">Waiting for debug info...</div>
|
| 74 |
-
<div>Image URL: <span id="imageUrl"></span></div>
|
| 75 |
-
<div>Last frame time: <span id="lastFrameTime"></span></div>
|
| 76 |
-
</div>
|
| 77 |
-
|
| 78 |
-
<div style="margin-top: 20px;">
|
| 79 |
-
<button onclick="testImage()">Test Image Load</button>
|
| 80 |
-
<button onclick="reloadPage()">Reload Page</button>
|
| 81 |
</div>
|
| 82 |
</div>
|
| 83 |
|
|
@@ -89,13 +101,11 @@ HTML = '''
|
|
| 89 |
function updateStream() {
|
| 90 |
const now = Date.now();
|
| 91 |
const img = document.getElementById('streamImg');
|
| 92 |
-
const timestamp = now;
|
| 93 |
|
| 94 |
-
// Update image with timestamp
|
| 95 |
-
img.src = '/stream?_=' +
|
| 96 |
-
document.getElementById('imageUrl').textContent = img.src;
|
| 97 |
|
| 98 |
-
// Calculate FPS
|
| 99 |
streamFrameCount++;
|
| 100 |
if (now - lastStreamUpdate >= 1000) {
|
| 101 |
const streamFps = streamFrameCount / ((now - lastStreamUpdate) / 1000);
|
|
@@ -110,37 +120,18 @@ HTML = '''
|
|
| 110 |
.then(data => {
|
| 111 |
document.getElementById('captureFps').textContent = data.capture_fps.toFixed(1);
|
| 112 |
document.getElementById('totalFrames').textContent = data.frame_count;
|
| 113 |
-
document.getElementById('debugInfo').textContent = data.debug_info || 'No debug info';
|
| 114 |
-
document.getElementById('lastFrameTime').textContent = new Date(data.timestamp * 1000).toLocaleTimeString();
|
| 115 |
|
| 116 |
if (lastFrameTimestamp > 0) {
|
| 117 |
const latency = now - (data.timestamp * 1000);
|
| 118 |
document.getElementById('latency').textContent = Math.round(latency) + ' ms';
|
| 119 |
}
|
| 120 |
lastFrameTimestamp = data.timestamp * 1000;
|
| 121 |
-
})
|
| 122 |
-
.catch(err => {
|
| 123 |
-
console.error('Stats fetch error:', err);
|
| 124 |
});
|
| 125 |
}
|
| 126 |
|
| 127 |
-
|
| 128 |
-
const img = document.getElementById('streamImg');
|
| 129 |
-
console.log('Testing image load...');
|
| 130 |
-
img.onload = () => console.log('β
Image loaded successfully');
|
| 131 |
-
img.onerror = () => console.log('β Image failed to load');
|
| 132 |
-
img.src = '/stream?_=' + Date.now();
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
function reloadPage() {
|
| 136 |
-
location.reload();
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
// Start polling
|
| 140 |
setInterval(updateStream, 33);
|
| 141 |
updateStream();
|
| 142 |
-
|
| 143 |
-
console.log('DEBUG: Streaming client started');
|
| 144 |
</script>
|
| 145 |
</body>
|
| 146 |
</html>
|
|
@@ -157,34 +148,16 @@ def stats():
|
|
| 157 |
'frame_count': shared['frame_count'],
|
| 158 |
'capture_fps': shared['capture_fps'],
|
| 159 |
'timestamp': time.time(),
|
| 160 |
-
'has_frame': shared['latest_frame'] is not None
|
| 161 |
-
'debug_info': shared['debug_info'],
|
| 162 |
-
'frame_size': len(shared['latest_frame']) if shared['latest_frame'] else 0
|
| 163 |
}
|
| 164 |
|
| 165 |
@app.route('/stream')
|
| 166 |
def stream():
|
| 167 |
-
"""Stream endpoint -
|
| 168 |
if shared['latest_frame']:
|
| 169 |
try:
|
| 170 |
-
#
|
| 171 |
-
frame_size = len(shared['latest_frame'])
|
| 172 |
-
print(f"DEBUG: Serving frame {shared['frame_count']}, size: {frame_size} bytes")
|
| 173 |
-
|
| 174 |
-
# Decode base64
|
| 175 |
image_data = base64.b64decode(shared['latest_frame'])
|
| 176 |
-
|
| 177 |
-
# Check if it's valid image data
|
| 178 |
-
if len(image_data) < 100: # Too small for a valid JPEG
|
| 179 |
-
print(f"WARNING: Image data too small: {len(image_data)} bytes")
|
| 180 |
-
# Return a test image
|
| 181 |
-
return Response(
|
| 182 |
-
b'Test image',
|
| 183 |
-
mimetype='text/plain',
|
| 184 |
-
headers={'Cache-Control': 'no-cache'}
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
# Return the image
|
| 188 |
return Response(
|
| 189 |
image_data,
|
| 190 |
mimetype='image/jpeg',
|
|
@@ -195,66 +168,47 @@ def stream():
|
|
| 195 |
}
|
| 196 |
)
|
| 197 |
except Exception as e:
|
| 198 |
-
print(f"
|
| 199 |
-
shared['debug_info'] = f"Stream error: {str(e)}"
|
| 200 |
|
| 201 |
-
# Return
|
| 202 |
-
|
| 203 |
-
import random
|
| 204 |
-
test_image = create_test_image()
|
| 205 |
-
return Response(
|
| 206 |
-
test_image,
|
| 207 |
-
mimetype='image/png',
|
| 208 |
-
headers={'Cache-Control': 'no-cache'}
|
| 209 |
-
)
|
| 210 |
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
import io as imageio
|
| 215 |
-
|
| 216 |
-
# Create a simple test image
|
| 217 |
-
img = Image.new('RGB', (400, 300), color='blue')
|
| 218 |
-
draw = ImageDraw.Draw(img)
|
| 219 |
-
draw.rectangle([50, 50, 350, 250], fill='red')
|
| 220 |
-
draw.text((150, 140), 'TEST', fill='white')
|
| 221 |
-
draw.text((120, 160), 'PyGame Stream', fill='white')
|
| 222 |
-
|
| 223 |
-
# Save to bytes
|
| 224 |
-
buffer = imageio.BytesIO()
|
| 225 |
-
img.save(buffer, format='PNG')
|
| 226 |
-
return buffer.getvalue()
|
| 227 |
-
|
| 228 |
-
# ===== 5. FIXED CAPTURE LOOP =====
|
| 229 |
-
def simple_capture_loop():
|
| 230 |
-
"""CAPTURE LOOP WITH FIXED IMAGE SAVING"""
|
| 231 |
|
| 232 |
try:
|
| 233 |
pygame.init()
|
| 234 |
-
print("β
PyGame initialized")
|
| 235 |
except Exception as e:
|
| 236 |
print(f"β PyGame init error: {e}")
|
| 237 |
-
shared['debug_info'] = f"PyGame init error: {e}"
|
| 238 |
return
|
| 239 |
|
| 240 |
# Create surfaces
|
| 241 |
screen = pygame.Surface((WIDTH, HEIGHT))
|
| 242 |
|
| 243 |
-
#
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
circle_r = 30
|
| 246 |
speed_x, speed_y = 4, 3
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
fps_update_time = time.time()
|
| 249 |
frames_since_update = 0
|
| 250 |
-
frame_num = 0
|
| 251 |
|
| 252 |
try:
|
| 253 |
while shared["streaming"]:
|
| 254 |
start_time = time.time()
|
| 255 |
-
frame_num += 1
|
| 256 |
|
| 257 |
-
#
|
| 258 |
circle_x += speed_x
|
| 259 |
circle_y += speed_y
|
| 260 |
|
|
@@ -263,54 +217,58 @@ def simple_capture_loop():
|
|
| 263 |
if circle_y - circle_r < 0 or circle_y + circle_r > HEIGHT:
|
| 264 |
speed_y *= -1
|
| 265 |
|
| 266 |
-
#
|
| 267 |
-
screen.fill((25, 25, 45))
|
|
|
|
|
|
|
| 268 |
pygame.draw.circle(screen, (255, 80, 80), (int(circle_x), int(circle_y)), circle_r)
|
| 269 |
pygame.draw.circle(screen, (255, 255, 255), (int(circle_x), int(circle_y)), circle_r, 2)
|
| 270 |
|
| 271 |
-
#
|
| 272 |
font = pygame.font.Font(None, 36)
|
| 273 |
-
text = font.render(f"Frame: {
|
| 274 |
screen.blit(text, (10, 10))
|
| 275 |
|
| 276 |
-
#
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
|
|
|
|
|
|
| 312 |
|
| 313 |
-
#
|
| 314 |
shared["frame_count"] += 1
|
| 315 |
frames_since_update += 1
|
| 316 |
shared["frames_this_second"] += 1
|
|
@@ -324,7 +282,7 @@ def simple_capture_loop():
|
|
| 324 |
|
| 325 |
shared["last_frame_time"] = current_time
|
| 326 |
|
| 327 |
-
#
|
| 328 |
elapsed = time.time() - start_time
|
| 329 |
target_time = 1.0 / TARGET_FPS
|
| 330 |
if elapsed < target_time:
|
|
@@ -332,31 +290,37 @@ def simple_capture_loop():
|
|
| 332 |
|
| 333 |
except Exception as e:
|
| 334 |
print(f"β Capture loop error: {e}")
|
| 335 |
-
|
|
|
|
| 336 |
|
| 337 |
pygame.quit()
|
| 338 |
-
print("π
|
| 339 |
|
| 340 |
# ===== 6. MAIN FUNCTION =====
|
| 341 |
def main():
|
| 342 |
print("=" * 60)
|
| 343 |
-
print("π PyGame +
|
| 344 |
print("=" * 60)
|
| 345 |
print(f"Port: {PORT}")
|
| 346 |
-
print(f"
|
| 347 |
print(f"Target FPS: {TARGET_FPS}")
|
|
|
|
| 348 |
print("=" * 60)
|
| 349 |
|
| 350 |
# Start capture thread
|
| 351 |
-
print("Starting capture thread...")
|
| 352 |
-
capture_thread = threading.Thread(target=
|
| 353 |
capture_thread.start()
|
| 354 |
|
| 355 |
-
# Wait
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
print(f"Starting Flask server on port {PORT}...")
|
| 359 |
-
print(f"Open: https://YOUR_SPACE.hf.space")
|
| 360 |
print("=" * 60)
|
| 361 |
|
| 362 |
try:
|
|
@@ -367,11 +331,14 @@ def main():
|
|
| 367 |
threaded=True,
|
| 368 |
use_reloader=False
|
| 369 |
)
|
|
|
|
|
|
|
| 370 |
except Exception as e:
|
| 371 |
-
print(f"Server error: {e}")
|
| 372 |
finally:
|
| 373 |
shared['streaming'] = False
|
| 374 |
capture_thread.join(timeout=2)
|
|
|
|
| 375 |
print("β
Shutdown complete")
|
| 376 |
|
| 377 |
if __name__ == "__main__":
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
# reliable_30fps_numpy.py - PyGame + Flask with NumPy surface arrays
|
| 3 |
import pygame
|
| 4 |
+
import numpy as np
|
| 5 |
import time
|
| 6 |
import threading
|
| 7 |
import base64
|
|
|
|
| 27 |
"capture_fps": 0,
|
| 28 |
"streaming": True,
|
| 29 |
"last_frame_time": time.time(),
|
| 30 |
+
"frames_this_second": 0
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
# ===== 2. CREATE FLASK APP =====
|
| 34 |
app = Flask(__name__)
|
| 35 |
|
| 36 |
+
# ===== 3. HTML TEMPLATE =====
|
| 37 |
HTML = '''
|
| 38 |
<!DOCTYPE html>
|
| 39 |
<html>
|
| 40 |
<head>
|
| 41 |
<meta charset="UTF-8">
|
| 42 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 43 |
+
<title>PyGame + NumPy Streaming</title>
|
| 44 |
<style>
|
| 45 |
body { font-family: Arial; background: #0f172a; color: white; padding: 20px; margin: 0; }
|
| 46 |
.container { max-width: 800px; margin: 0 auto; text-align: center; }
|
| 47 |
h1 { color: #60a5fa; }
|
| 48 |
+
.image-container {
|
| 49 |
+
width: 400px;
|
| 50 |
+
height: 300px;
|
| 51 |
+
margin: 20px auto;
|
| 52 |
+
border: 3px solid #60a5fa;
|
| 53 |
+
background: #000;
|
| 54 |
+
overflow: hidden;
|
| 55 |
+
}
|
| 56 |
+
#streamImg {
|
| 57 |
+
width: 100%;
|
| 58 |
+
height: 100%;
|
| 59 |
+
object-fit: contain;
|
| 60 |
+
image-rendering: pixelated;
|
| 61 |
+
}
|
| 62 |
+
.stats {
|
| 63 |
+
display: inline-block;
|
| 64 |
+
background: #1e293b;
|
| 65 |
+
padding: 20px;
|
| 66 |
+
border-radius: 10px;
|
| 67 |
+
margin: 10px;
|
| 68 |
+
text-align: left;
|
| 69 |
+
}
|
| 70 |
+
.stat-row { margin: 10px 0; display: flex; justify-content: space-between; min-width: 200px; }
|
| 71 |
.stat-value { font-weight: bold; color: #60a5fa; }
|
| 72 |
+
.info { color: #94a3b8; margin-top: 20px; font-size: 0.9em; }
|
| 73 |
</style>
|
| 74 |
</head>
|
| 75 |
<body>
|
| 76 |
<div class="container">
|
| 77 |
+
<h1>PyGame + NumPy Surface Array Streaming</h1>
|
| 78 |
+
<p class="info">Using NumPy arrays for pixel manipulation</p>
|
| 79 |
|
| 80 |
<div class="image-container">
|
| 81 |
+
<img id="streamImg" src="/stream" alt="Live Stream">
|
| 82 |
</div>
|
| 83 |
|
| 84 |
<div class="stats">
|
|
|
|
| 88 |
<div class="stat-row"><span>Latency:</span><span class="stat-value" id="latency">-- ms</span></div>
|
| 89 |
</div>
|
| 90 |
|
| 91 |
+
<div class="info">
|
| 92 |
+
Rendering with NumPy arrays β’ Port: ''' + str(PORT) + ''' β’ Target: 30 FPS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
</div>
|
| 94 |
</div>
|
| 95 |
|
|
|
|
| 101 |
function updateStream() {
|
| 102 |
const now = Date.now();
|
| 103 |
const img = document.getElementById('streamImg');
|
|
|
|
| 104 |
|
| 105 |
+
// Update image with anti-cache timestamp
|
| 106 |
+
img.src = '/stream?_=' + now;
|
|
|
|
| 107 |
|
| 108 |
+
// Calculate stream FPS
|
| 109 |
streamFrameCount++;
|
| 110 |
if (now - lastStreamUpdate >= 1000) {
|
| 111 |
const streamFps = streamFrameCount / ((now - lastStreamUpdate) / 1000);
|
|
|
|
| 120 |
.then(data => {
|
| 121 |
document.getElementById('captureFps').textContent = data.capture_fps.toFixed(1);
|
| 122 |
document.getElementById('totalFrames').textContent = data.frame_count;
|
|
|
|
|
|
|
| 123 |
|
| 124 |
if (lastFrameTimestamp > 0) {
|
| 125 |
const latency = now - (data.timestamp * 1000);
|
| 126 |
document.getElementById('latency').textContent = Math.round(latency) + ' ms';
|
| 127 |
}
|
| 128 |
lastFrameTimestamp = data.timestamp * 1000;
|
|
|
|
|
|
|
|
|
|
| 129 |
});
|
| 130 |
}
|
| 131 |
|
| 132 |
+
// Start polling at 30 FPS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
setInterval(updateStream, 33);
|
| 134 |
updateStream();
|
|
|
|
|
|
|
| 135 |
</script>
|
| 136 |
</body>
|
| 137 |
</html>
|
|
|
|
| 148 |
'frame_count': shared['frame_count'],
|
| 149 |
'capture_fps': shared['capture_fps'],
|
| 150 |
'timestamp': time.time(),
|
| 151 |
+
'has_frame': shared['latest_frame'] is not None
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
@app.route('/stream')
|
| 155 |
def stream():
|
| 156 |
+
"""Stream endpoint - returns the latest NumPy/surface frame."""
|
| 157 |
if shared['latest_frame']:
|
| 158 |
try:
|
| 159 |
+
# Decode base64 and serve
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
image_data = base64.b64decode(shared['latest_frame'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
return Response(
|
| 162 |
image_data,
|
| 163 |
mimetype='image/jpeg',
|
|
|
|
| 168 |
}
|
| 169 |
)
|
| 170 |
except Exception as e:
|
| 171 |
+
print(f"Stream error: {e}")
|
|
|
|
| 172 |
|
| 173 |
+
# Return empty response if no frame
|
| 174 |
+
return Response(b'', mimetype='image/jpeg')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
+
# ===== 5. NUMPY SURFACE CAPTURE LOOP =====
|
| 177 |
+
def numpy_capture_loop():
|
| 178 |
+
"""Capture loop using NumPy arrays for pixel manipulation."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
try:
|
| 181 |
pygame.init()
|
| 182 |
+
print("β
PyGame initialized for NumPy rendering")
|
| 183 |
except Exception as e:
|
| 184 |
print(f"β PyGame init error: {e}")
|
|
|
|
| 185 |
return
|
| 186 |
|
| 187 |
# Create surfaces
|
| 188 |
screen = pygame.Surface((WIDTH, HEIGHT))
|
| 189 |
|
| 190 |
+
# Create NumPy array for direct pixel access (optional, for advanced effects)
|
| 191 |
+
# This creates a 3D array: [height, width, 3] for RGB
|
| 192 |
+
pixel_array = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
|
| 193 |
+
|
| 194 |
+
# Animation variables
|
| 195 |
+
circle_x, circle_y = WIDTH // 2, HEIGHT // 2
|
| 196 |
circle_r = 30
|
| 197 |
speed_x, speed_y = 4, 3
|
| 198 |
|
| 199 |
+
# For gradient effect using NumPy
|
| 200 |
+
x_coords = np.arange(WIDTH)
|
| 201 |
+
y_coords = np.arange(HEIGHT)
|
| 202 |
+
X, Y = np.meshgrid(x_coords, y_coords)
|
| 203 |
+
|
| 204 |
fps_update_time = time.time()
|
| 205 |
frames_since_update = 0
|
|
|
|
| 206 |
|
| 207 |
try:
|
| 208 |
while shared["streaming"]:
|
| 209 |
start_time = time.time()
|
|
|
|
| 210 |
|
| 211 |
+
# === UPDATE POSITION ===
|
| 212 |
circle_x += speed_x
|
| 213 |
circle_y += speed_y
|
| 214 |
|
|
|
|
| 217 |
if circle_y - circle_r < 0 or circle_y + circle_r > HEIGHT:
|
| 218 |
speed_y *= -1
|
| 219 |
|
| 220 |
+
# === METHOD 1: Traditional PyGame drawing (simpler) ===
|
| 221 |
+
screen.fill((25, 25, 45)) # Dark blue background
|
| 222 |
+
|
| 223 |
+
# Draw bouncing circle
|
| 224 |
pygame.draw.circle(screen, (255, 80, 80), (int(circle_x), int(circle_y)), circle_r)
|
| 225 |
pygame.draw.circle(screen, (255, 255, 255), (int(circle_x), int(circle_y)), circle_r, 2)
|
| 226 |
|
| 227 |
+
# Draw frame counter
|
| 228 |
font = pygame.font.Font(None, 36)
|
| 229 |
+
text = font.render(f"Frame: {shared['frame_count']}", True, (100, 255, 100))
|
| 230 |
screen.blit(text, (10, 10))
|
| 231 |
|
| 232 |
+
# Draw FPS
|
| 233 |
+
fps_text = font.render(f"FPS: {shared['capture_fps']:.1f}", True, (100, 255, 100))
|
| 234 |
+
screen.blit(fps_text, (10, 50))
|
| 235 |
+
|
| 236 |
+
# === OPTIONAL: METHOD 2: NumPy pixel manipulation ===
|
| 237 |
+
# Uncomment this for advanced NumPy effects
|
| 238 |
+
"""
|
| 239 |
+
# Clear with gradient using NumPy
|
| 240 |
+
gradient = 25 + (X * 0.03 + Y * 0.02).astype(np.uint8)
|
| 241 |
+
pixel_array[:, :, 0] = gradient # Red channel
|
| 242 |
+
pixel_array[:, :, 1] = gradient // 2 # Green channel
|
| 243 |
+
pixel_array[:, :, 2] = 45 # Blue channel
|
| 244 |
+
|
| 245 |
+
# Draw circle using NumPy (distance formula)
|
| 246 |
+
distance = np.sqrt((X - circle_x)**2 + (Y - circle_y)**2)
|
| 247 |
+
circle_mask = distance < circle_r
|
| 248 |
+
|
| 249 |
+
# Set circle pixels to red
|
| 250 |
+
pixel_array[circle_mask, 0] = 255 # Red
|
| 251 |
+
pixel_array[circle_mask, 1] = 80 # Green
|
| 252 |
+
pixel_array[circle_mask, 2] = 80 # Blue
|
| 253 |
+
|
| 254 |
+
# Convert NumPy array to PyGame surface
|
| 255 |
+
pygame.surfarray.blit_array(screen, pixel_array)
|
| 256 |
+
"""
|
| 257 |
+
|
| 258 |
+
# === SCALE AND SAVE FRAME ===
|
| 259 |
+
# Scale down for streaming
|
| 260 |
+
scaled = pygame.transform.smoothscale(screen, (STREAM_WIDTH, STREAM_HEIGHT))
|
| 261 |
+
|
| 262 |
+
# Save to memory buffer
|
| 263 |
+
buffer = io.BytesIO()
|
| 264 |
+
pygame.image.save(scaled, buffer, 'JPEG')
|
| 265 |
+
buffer.seek(0)
|
| 266 |
+
|
| 267 |
+
# Encode to base64 for sharing
|
| 268 |
+
image_bytes = buffer.getvalue()
|
| 269 |
+
shared["latest_frame"] = base64.b64encode(image_bytes).decode('utf-8')
|
| 270 |
|
| 271 |
+
# === UPDATE COUNTERS ===
|
| 272 |
shared["frame_count"] += 1
|
| 273 |
frames_since_update += 1
|
| 274 |
shared["frames_this_second"] += 1
|
|
|
|
| 282 |
|
| 283 |
shared["last_frame_time"] = current_time
|
| 284 |
|
| 285 |
+
# === FRAME RATE CONTROL ===
|
| 286 |
elapsed = time.time() - start_time
|
| 287 |
target_time = 1.0 / TARGET_FPS
|
| 288 |
if elapsed < target_time:
|
|
|
|
| 290 |
|
| 291 |
except Exception as e:
|
| 292 |
print(f"β Capture loop error: {e}")
|
| 293 |
+
import traceback
|
| 294 |
+
traceback.print_exc()
|
| 295 |
|
| 296 |
pygame.quit()
|
| 297 |
+
print("π NumPy capture loop stopped")
|
| 298 |
|
| 299 |
# ===== 6. MAIN FUNCTION =====
|
| 300 |
def main():
|
| 301 |
print("=" * 60)
|
| 302 |
+
print("π PyGame + NumPy Surface Array Streaming")
|
| 303 |
print("=" * 60)
|
| 304 |
print(f"Port: {PORT}")
|
| 305 |
+
print(f"Render: {WIDTH}x{HEIGHT} β Stream: {STREAM_WIDTH}x{STREAM_HEIGHT}")
|
| 306 |
print(f"Target FPS: {TARGET_FPS}")
|
| 307 |
+
print("Using NumPy for pixel manipulation")
|
| 308 |
print("=" * 60)
|
| 309 |
|
| 310 |
# Start capture thread
|
| 311 |
+
print("Starting NumPy capture thread...")
|
| 312 |
+
capture_thread = threading.Thread(target=numpy_capture_loop, daemon=True)
|
| 313 |
capture_thread.start()
|
| 314 |
|
| 315 |
+
# Wait for first frame
|
| 316 |
+
print("Waiting for first frame...")
|
| 317 |
+
for i in range(30):
|
| 318 |
+
if shared['latest_frame']:
|
| 319 |
+
print(f"β
First frame ready after {i*0.1:.1f}s")
|
| 320 |
+
break
|
| 321 |
+
time.sleep(0.1)
|
| 322 |
|
| 323 |
print(f"Starting Flask server on port {PORT}...")
|
|
|
|
| 324 |
print("=" * 60)
|
| 325 |
|
| 326 |
try:
|
|
|
|
| 331 |
threaded=True,
|
| 332 |
use_reloader=False
|
| 333 |
)
|
| 334 |
+
except KeyboardInterrupt:
|
| 335 |
+
print("\nπ Interrupted")
|
| 336 |
except Exception as e:
|
| 337 |
+
print(f"β Server error: {e}")
|
| 338 |
finally:
|
| 339 |
shared['streaming'] = False
|
| 340 |
capture_thread.join(timeout=2)
|
| 341 |
+
print(f"\nπ Final: {shared['frame_count']} frames @ {shared['capture_fps']:.1f} FPS")
|
| 342 |
print("β
Shutdown complete")
|
| 343 |
|
| 344 |
if __name__ == "__main__":
|