Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| # shared_memory_working.py - Working shared memory | |
| import pygame | |
| import numpy as np | |
| import time | |
| import threading | |
| import os | |
| from flask import Flask, Response | |
| # ===== 1. SETUP ===== | |
| os.environ['SDL_VIDEODRIVER'] = 'dummy' | |
| os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' | |
| PORT = int(os.getenv('PORT', 7860)) | |
| WIDTH, HEIGHT = 400, 300 | |
| print(f"π Starting on port {PORT}") | |
| # ===== 2. SHARED MEMORY WITH DEFAULT DATA ===== | |
| # Create initial frame data immediately (BEFORE any threads start) | |
| initial_frame = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8) | |
| initial_frame[:, :] = [100, 150, 200] # Light blue | |
| initial_bytes = initial_frame.tobytes() | |
| shared_state = { | |
| "raw_pixels": initial_bytes, # ALWAYS has data from the start | |
| "frame_count": 0, | |
| "streaming": True, | |
| "lock": threading.Lock(), | |
| "ready": True # Mark as ready immediately | |
| } | |
| # ===== 3. FLASK APP ===== | |
| app = Flask(__name__) | |
| # ===== 4. SIMPLE HTML WITH VISIBLE TEST ===== | |
| HTML = '''<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Shared Memory WORKING</title> | |
| <style> | |
| body { margin: 0; padding: 20px; background: #111; color: white; font-family: Arial; } | |
| h1 { color: #4CAF50; text-align: center; } | |
| .test-area { | |
| text-align: center; | |
| margin: 30px 0; | |
| padding: 20px; | |
| background: #222; | |
| border-radius: 10px; | |
| } | |
| #streamCanvas { | |
| border: 5px solid #4CAF50; | |
| background: black; | |
| display: block; | |
| margin: 20px auto; | |
| } | |
| .stats { | |
| background: #333; | |
| padding: 15px; | |
| border-radius: 8px; | |
| display: inline-block; | |
| margin: 10px; | |
| } | |
| .success { color: #4CAF50; font-weight: bold; } | |
| .error { color: #f44336; font-weight: bold; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>π― SHARED MEMORY TEST - MUST SHOW IMAGE</h1> | |
| <div class="test-area"> | |
| <h2>Canvas should show colors below:</h2> | |
| <canvas id="streamCanvas" width="400" height="300"></canvas> | |
| <div class="stats"> | |
| <div>Server Status: <span id="serverStatus" class="success">Connected</span></div> | |
| <div>Frames Received: <span id="framesReceived">0</span></div> | |
| <div>Last Update: <span id="lastUpdate">Never</span></div> | |
| </div> | |
| <div style="margin-top: 20px;"> | |
| <button onclick="testConnection()">Test Connection</button> | |
| <button onclick="forceRefresh()">Force Refresh</button> | |
| </div> | |
| </div> | |
| <div style="text-align: center; color: #888; margin-top: 30px;"> | |
| If you see a colored canvas above, it's working!<br> | |
| If it's black, check browser console (F12) for errors. | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('streamCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let receivedFrames = 0; | |
| // Initialize with a test pattern immediately | |
| function drawTestPattern() { | |
| const testImage = new ImageData(400, 300); | |
| const data = testImage.data; | |
| // Create a red/gradient test pattern | |
| for (let y = 0; y < 300; y++) { | |
| for (let x = 0; x < 400; x++) { | |
| const i = (y * 400 + x) * 4; | |
| data[i] = x % 256; // Red gradient | |
| data[i + 1] = y % 256; // Green gradient | |
| data[i + 2] = 128; // Constant blue | |
| data[i + 3] = 255; // Alpha | |
| } | |
| } | |
| ctx.putImageData(testImage, 0, 0); | |
| console.log('β Drawn test pattern'); | |
| } | |
| // Draw test pattern initially | |
| drawTestPattern(); | |
| function updateFrame() { | |
| const startTime = Date.now(); | |
| fetch('/frame') | |
| .then(response => { | |
| console.log('Response status:', response.status, 'type:', response.headers.get('content-type')); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
| } | |
| return response.arrayBuffer(); | |
| }) | |
| .then(buffer => { | |
| receivedFrames++; | |
| const now = new Date().toLocaleTimeString(); | |
| // Update stats | |
| document.getElementById('framesReceived').textContent = receivedFrames; | |
| document.getElementById('lastUpdate').textContent = now; | |
| // Check buffer size | |
| console.log(`π¦ Received ${buffer.byteLength} bytes`); | |
| if (buffer.byteLength === 0) { | |
| console.warn('β οΈ Empty buffer received'); | |
| return; | |
| } | |
| // Create ImageData | |
| const imageData = new ImageData(400, 300); | |
| const rgba = imageData.data; | |
| // Convert RGB to RGBA | |
| const rgb = new Uint8Array(buffer); | |
| // Verify size | |
| const expectedSize = 400 * 300 * 3; | |
| if (rgb.length !== expectedSize) { | |
| console.error(`β Wrong size: ${rgb.length} bytes, expected ${expectedSize}`); | |
| return; | |
| } | |
| // Fast conversion | |
| for (let i = 0, j = 0; i < rgba.length; i += 4, j += 3) { | |
| rgba[i] = rgb[j]; | |
| rgba[i + 1] = rgb[j + 1]; | |
| rgba[i + 2] = rgb[j + 2]; | |
| rgba[i + 3] = 255; | |
| } | |
| // Draw to canvas | |
| ctx.putImageData(imageData, 0, 0); | |
| const latency = Date.now() - startTime; | |
| console.log(`β Frame ${receivedFrames} drawn, latency: ${latency}ms`); | |
| }) | |
| .catch(error => { | |
| console.error('β Frame fetch error:', error); | |
| document.getElementById('serverStatus').textContent = 'Error: ' + error.message; | |
| document.getElementById('serverStatus').className = 'error'; | |
| // Draw error pattern | |
| const errorImage = new ImageData(400, 300); | |
| const data = errorImage.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| data[i] = 100; // R | |
| data[i + 1] = 0; // G | |
| data[i + 2] = 0; // B | |
| data[i + 3] = 255; // A | |
| } | |
| ctx.putImageData(errorImage, 0, 0); | |
| }); | |
| } | |
| function testConnection() { | |
| console.log('π Testing connection...'); | |
| fetch('/test') | |
| .then(r => r.json()) | |
| .then(data => { | |
| console.log('Server test response:', data); | |
| alert('Server OK: ' + JSON.stringify(data)); | |
| }) | |
| .catch(err => { | |
| console.error('Connection test failed:', err); | |
| alert('Connection failed: ' + err); | |
| }); | |
| } | |
| function forceRefresh() { | |
| console.log('π Forcing refresh...'); | |
| updateFrame(); | |
| } | |
| // Start updating every second (1 FPS for testing) | |
| setInterval(updateFrame, 1000); | |
| // Initial update | |
| setTimeout(updateFrame, 500); | |
| console.log('π’ Page loaded and ready'); | |
| </script> | |
| </body> | |
| </html>''' | |
| def index(): | |
| return HTML | |
| def test(): | |
| """Simple test endpoint""" | |
| with shared_state['lock']: | |
| return { | |
| 'status': 'ok', | |
| 'frame_count': shared_state['frame_count'], | |
| 'data_size': len(shared_state['raw_pixels']) if shared_state['raw_pixels'] else 0, | |
| 'time': time.time() | |
| } | |
| def get_frame(): | |
| """Return frame data - ALWAYS returns data""" | |
| with shared_state['lock']: | |
| # ALWAYS return data, even if it's the initial frame | |
| data = shared_state['raw_pixels'] | |
| frame_num = shared_state['frame_count'] | |
| print(f"π€ Serving frame {frame_num}, {len(data) if data else 0} bytes") | |
| if data is None: | |
| # This should never happen, but just in case | |
| print("β οΈ WARNING: No frame data, sending fallback") | |
| fallback = np.ones((HEIGHT, WIDTH, 3), dtype=np.uint8) * [255, 0, 0] # Red | |
| data = fallback.tobytes() | |
| return Response( | |
| data, | |
| mimetype='application/octet-stream', | |
| headers={ | |
| 'Cache-Control': 'no-cache, no-store, must-revalidate', | |
| 'Pragma': 'no-cache', | |
| 'Expires': '0', | |
| 'Content-Type': 'application/octet-stream', | |
| 'X-Frame-Number': str(frame_num) | |
| } | |
| ) | |
| # ===== 5. RENDER THREAD - UPDATES SHARED MEMORY ===== | |
| def render_thread(): | |
| """Update the shared memory with new frames""" | |
| print("π¬ Starting render thread...") | |
| try: | |
| pygame.init() | |
| print("β PyGame initialized") | |
| except Exception as e: | |
| print(f"β PyGame init failed: {e}") | |
| # Continue anyway - we already have initial frame | |
| # Create surface | |
| surface = pygame.Surface((WIDTH, HEIGHT)) | |
| # Get pixel array | |
| pixels_3d = pygame.surfarray.pixels3d(surface) | |
| # Simple animation | |
| x, y = WIDTH // 2, HEIGHT // 2 | |
| dx, dy = 3, 2 | |
| color_cycle = 0 | |
| while shared_state['streaming']: | |
| try: | |
| # Clear with gradient | |
| for y_pos in range(HEIGHT): | |
| gradient = 50 + int(y_pos / HEIGHT * 100) | |
| pixels_3d[y_pos, :, 0] = gradient # Red | |
| pixels_3d[y_pos, :, 1] = gradient // 2 # Green | |
| pixels_3d[y_pos, :, 2] = 100 # Blue | |
| # Update position | |
| x += dx | |
| y += dy | |
| if x < 20 or x > WIDTH - 20: | |
| dx = -dx | |
| color_cycle = (color_cycle + 1) % 3 | |
| if y < 20 or y > HEIGHT - 20: | |
| dy = -dy | |
| color_cycle = (color_cycle + 1) % 3 | |
| # Draw ball | |
| colors = [[255, 100, 100], [100, 255, 100], [100, 100, 255]] | |
| current_color = colors[color_cycle] | |
| # Simple circle drawing | |
| for dy_circle in range(-20, 21): | |
| for dx_circle in range(-20, 21): | |
| if dx_circle*dx_circle + dy_circle*dy_circle <= 20*20: | |
| px = x + dx_circle | |
| py = y + dy_circle | |
| if 0 <= px < WIDTH and 0 <= py < HEIGHT: | |
| pixels_3d[py, px] = current_color | |
| # Add text | |
| font = pygame.font.Font(None, 24) | |
| with shared_state['lock']: | |
| frame_num = shared_state['frame_count'] | |
| time_text = font.render(time.strftime("%H:%M:%S"), True, (255, 255, 255)) | |
| frame_text = font.render(f"Frame: {frame_num}", True, (255, 255, 255)) | |
| # Blit text | |
| surface.blit(time_text, (10, 10)) | |
| surface.blit(frame_text, (10, 40)) | |
| # Convert to bytes | |
| raw_bytes = pixels_3d.tobytes() | |
| # Update shared memory | |
| with shared_state['lock']: | |
| shared_state['raw_pixels'] = raw_bytes | |
| shared_state['frame_count'] += 1 | |
| if frame_num % 30 == 0: | |
| print(f"πΈ Rendered frame {frame_num}") | |
| time.sleep(1/30) # 30 FPS | |
| except Exception as e: | |
| print(f"β οΈ Render error: {e}") | |
| time.sleep(0.1) | |
| print("π Render thread stopped") | |
| # ===== 6. MAIN ===== | |
| def main(): | |
| print("="*60) | |
| print("π― SHARED MEMORY SERVER STARTING") | |
| print("="*60) | |
| print(f"π‘ Port: {PORT}") | |
| print(f"π¨ Frame size: {WIDTH}x{HEIGHT}x3 = {WIDTH*HEIGHT*3} bytes") | |
| print(f"π Initial data ready: {len(shared_state['raw_pixels'])} bytes") | |
| print("="*60) | |
| # Start render thread | |
| renderer = threading.Thread(target=render_thread, daemon=True) | |
| renderer.start() | |
| # Give renderer time to start | |
| time.sleep(0.5) | |
| print("π Starting Flask server...") | |
| try: | |
| app.run( | |
| host='0.0.0.0', | |
| port=PORT, | |
| debug=False, | |
| threaded=True, | |
| use_reloader=False | |
| ) | |
| except Exception as e: | |
| print(f"β Server error: {e}") | |
| finally: | |
| shared_state['streaming'] = False | |
| renderer.join(timeout=1) | |
| print("β Server stopped") | |
| if __name__ == "__main__": | |
| main() |