MySafeCode commited on
Commit
cbc54ba
·
verified ·
1 Parent(s): 4e57227

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +572 -0
app.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pygame
2
+ import numpy as np
3
+ from flask import Flask, Response, render_template_string
4
+ from flask_sock import Sock
5
+ import time
6
+ import os
7
+ import cv2
8
+ import threading
9
+ import json
10
+ import base64
11
+ from PIL import Image
12
+ import io
13
+
14
+ # Initialize Pygame headlessly
15
+ os.environ['SDL_VIDEODRIVER'] = 'dummy'
16
+ pygame.init()
17
+
18
+ try:
19
+ pygame.mixer.init(frequency=44100, size=-16, channels=2)
20
+ print("✅ Audio mixer initialized")
21
+ except Exception as e:
22
+ print(f"⚠️ Audio mixer not available: {e}")
23
+
24
+ app = Flask(__name__)
25
+ sock = Sock(app)
26
+
27
+ class ShaderRenderer:
28
+ def __init__(self, width=640, height=480):
29
+ self.width = width
30
+ self.height = height
31
+ self.mouse_x = width // 2
32
+ self.mouse_y = height // 2
33
+ self.start_time = time.time()
34
+ self.surface = pygame.Surface((width, height))
35
+ self.frame_count = 0
36
+ self.last_frame_time = time.time()
37
+ self.fps = 0
38
+ self.button_clicked = False
39
+ self.sound_source = 'none'
40
+
41
+ def set_mouse(self, x, y):
42
+ self.mouse_x = max(0, min(self.width, x))
43
+ self.mouse_y = max(0, min(self.height, y))
44
+
45
+ def handle_click(self, x, y):
46
+ button_rect = pygame.Rect(self.width-200, 120, 180, 40)
47
+ if button_rect.collidepoint(x, y):
48
+ self.button_clicked = not self.button_clicked
49
+ return True
50
+ return False
51
+
52
+ def render_frame(self):
53
+ t = time.time() - self.start_time
54
+
55
+ # Calculate FPS
56
+ self.frame_count += 1
57
+ if time.time() - self.last_frame_time > 1.0:
58
+ self.fps = self.frame_count
59
+ self.frame_count = 0
60
+ self.last_frame_time = time.time()
61
+
62
+ # Clear
63
+ self.surface.fill((20, 20, 30))
64
+ font = pygame.font.Font(None, 24)
65
+
66
+ # Draw TOP marker
67
+ pygame.draw.rect(self.surface, (255, 100, 100), (10, 10, 100, 30))
68
+ text = font.render("TOP", True, (255, 255, 255))
69
+ self.surface.blit(text, (20, 15))
70
+
71
+ # Draw BOTTOM marker
72
+ pygame.draw.rect(self.surface, (100, 255, 100), (10, self.height-40, 100, 30))
73
+ text = font.render("BOTTOM", True, (0, 0, 0))
74
+ self.surface.blit(text, (20, self.height-35))
75
+
76
+ # Draw CLOCK
77
+ current_time = time.time()
78
+ seconds = int(current_time) % 60
79
+ hundredths = int((current_time * 100) % 100)
80
+ time_str = f"{seconds:02d}.{hundredths:02d}s"
81
+ clock_text = font.render(time_str, True, (0, 255, 255))
82
+ self.surface.blit(clock_text, (self.width-150, 40))
83
+
84
+ # Draw BUTTON
85
+ button_rect = pygame.Rect(self.width-200, 120, 180, 40)
86
+ mouse_over = button_rect.collidepoint(self.mouse_x, self.mouse_y)
87
+
88
+ if self.button_clicked:
89
+ button_color = (0, 200, 0)
90
+ elif mouse_over:
91
+ button_color = (100, 100, 200)
92
+ else:
93
+ button_color = (80, 80, 80)
94
+
95
+ pygame.draw.rect(self.surface, button_color, button_rect)
96
+ pygame.draw.rect(self.surface, (200, 200, 200), button_rect, 2)
97
+
98
+ btn_text = "✅ CLICKED!" if self.button_clicked else "🔘 CLICK ME"
99
+ text_surf = font.render(btn_text, True, (255, 255, 255))
100
+ text_rect = text_surf.get_rect(center=button_rect.center)
101
+ self.surface.blit(text_surf, text_rect)
102
+
103
+ # Draw circle
104
+ circle_size = 30 + int(20 * np.sin(t * 2))
105
+ if self.sound_source == 'pygame':
106
+ color = (100, 255, 100)
107
+ elif self.sound_source == 'browser':
108
+ color = (100, 100, 255)
109
+ else:
110
+ color = (255, 100, 100)
111
+
112
+ pygame.draw.circle(self.surface, color,
113
+ (self.mouse_x, self.mouse_y), circle_size)
114
+
115
+ # Draw grid
116
+ for x in range(0, self.width, 50):
117
+ alpha = int(40 + 20 * np.sin(x * 0.1 + t))
118
+ pygame.draw.line(self.surface, (alpha, alpha, 50),
119
+ (x, 0), (x, self.height))
120
+ for y in range(0, self.height, 50):
121
+ alpha = int(40 + 20 * np.cos(y * 0.1 + t))
122
+ pygame.draw.line(self.surface, (alpha, alpha, 50),
123
+ (0, y), (self.width, y))
124
+
125
+ # FPS counter
126
+ fps_text = font.render(f"FPS: {self.fps}", True, (255, 255, 0))
127
+ self.surface.blit(fps_text, (self.width-150, self.height-60))
128
+
129
+ return pygame.image.tostring(self.surface, 'RGB')
130
+
131
+ def get_frame_jpeg(self, quality=70):
132
+ """Return frame as JPEG bytes"""
133
+ frame = self.render_frame()
134
+ img = np.frombuffer(frame, dtype=np.uint8).reshape((self.height, self.width, 3))
135
+ img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
136
+ _, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, quality])
137
+ return jpeg.tobytes()
138
+
139
+ renderer = ShaderRenderer()
140
+
141
+ # Single WebSocket for everything
142
+ @sock.route('/ws')
143
+ def websocket(ws):
144
+ """One WebSocket to rule them all - video + interactions"""
145
+ print("🟢 Client connected")
146
+ running = True
147
+
148
+ # Video streaming thread
149
+ def video_stream():
150
+ while running:
151
+ try:
152
+ # Get frame as JPEG
153
+ frame = renderer.get_frame_jpeg(quality=70)
154
+
155
+ # Send as binary with a 6-byte header 'FRAME:'
156
+ # This helps client identify what type of data it is
157
+ header = b'FRAME:'
158
+ ws.send(header + frame)
159
+
160
+ time.sleep(1/30) # 30fps max
161
+ except:
162
+ break
163
+
164
+ # Start video thread
165
+ video_thread = threading.Thread(target=video_stream, daemon=True)
166
+ video_thread.start()
167
+
168
+ # Handle incoming messages (interactions)
169
+ try:
170
+ while True:
171
+ message = ws.receive()
172
+ if not message:
173
+ continue
174
+
175
+ # Parse JSON message
176
+ try:
177
+ data = json.loads(message)
178
+
179
+ if data['type'] == 'mouse':
180
+ renderer.set_mouse(data['x'], data['y'])
181
+
182
+ elif data['type'] == 'click':
183
+ renderer.handle_click(data['x'], data['y'])
184
+
185
+ elif data['type'] == 'sound':
186
+ renderer.sound_source = data['source']
187
+
188
+ # Optional: Send confirmation or state back
189
+ ws.send(json.dumps({
190
+ 'type': 'state',
191
+ 'button': renderer.button_clicked,
192
+ 'sound': renderer.sound_source
193
+ }))
194
+
195
+ except json.JSONDecodeError:
196
+ print(f"Invalid JSON: {message}")
197
+
198
+ except Exception as e:
199
+ print(f"Connection error: {e}")
200
+ finally:
201
+ running = False
202
+ print("🔴 Client disconnected")
203
+
204
+ @app.route('/')
205
+ def index():
206
+ return render_template_string('''
207
+ <!DOCTYPE html>
208
+ <html>
209
+ <head>
210
+ <title>🎮 Pygame + WebSocket Only</title>
211
+ <style>
212
+ body {
213
+ margin: 0;
214
+ background: #0a0a0a;
215
+ color: white;
216
+ font-family: 'Segoe UI', Arial, sans-serif;
217
+ display: flex;
218
+ justify-content: center;
219
+ align-items: center;
220
+ min-height: 100vh;
221
+ }
222
+ .container {
223
+ max-width: 900px;
224
+ padding: 20px;
225
+ text-align: center;
226
+ }
227
+ h1 { color: #4CAF50; margin-bottom: 20px; }
228
+ h1 small { font-size: 14px; color: #666; display: block; }
229
+
230
+ .video-container {
231
+ background: #000;
232
+ border-radius: 12px;
233
+ padding: 5px;
234
+ margin: 20px 0;
235
+ box-shadow: 0 0 30px rgba(76, 175, 80, 0.2);
236
+ position: relative;
237
+ }
238
+
239
+ canvas {
240
+ width: 100%;
241
+ max-width: 640px;
242
+ height: auto;
243
+ border-radius: 8px;
244
+ display: block;
245
+ margin: 0 auto;
246
+ background: #111;
247
+ cursor: crosshair;
248
+ }
249
+
250
+ .mouse-coords {
251
+ position: absolute;
252
+ bottom: 10px;
253
+ left: 10px;
254
+ background: rgba(0,0,0,0.7);
255
+ color: #4CAF50;
256
+ padding: 5px 10px;
257
+ border-radius: 20px;
258
+ font-family: monospace;
259
+ font-size: 14px;
260
+ pointer-events: none;
261
+ z-index: 10;
262
+ }
263
+
264
+ .controls {
265
+ background: #1a1a1a;
266
+ border-radius: 12px;
267
+ padding: 20px;
268
+ margin-top: 20px;
269
+ }
270
+
271
+ .sound-buttons {
272
+ display: flex;
273
+ gap: 10px;
274
+ justify-content: center;
275
+ margin: 20px 0;
276
+ flex-wrap: wrap;
277
+ }
278
+
279
+ button {
280
+ background: #333;
281
+ color: white;
282
+ border: none;
283
+ padding: 12px 24px;
284
+ font-size: 16px;
285
+ border-radius: 8px;
286
+ cursor: pointer;
287
+ transition: all 0.3s;
288
+ min-width: 100px;
289
+ font-weight: bold;
290
+ border: 1px solid #444;
291
+ }
292
+
293
+ button:hover {
294
+ transform: translateY(-2px);
295
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
296
+ }
297
+
298
+ button.active {
299
+ background: #4CAF50;
300
+ border-color: #4CAF50;
301
+ box-shadow: 0 0 20px #4CAF50;
302
+ }
303
+
304
+ .status-panel {
305
+ background: #222;
306
+ border-radius: 8px;
307
+ padding: 15px;
308
+ margin-top: 20px;
309
+ display: flex;
310
+ justify-content: space-around;
311
+ flex-wrap: wrap;
312
+ gap: 15px;
313
+ }
314
+
315
+ .status-item {
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 10px;
319
+ }
320
+
321
+ .status-label {
322
+ color: #888;
323
+ font-size: 14px;
324
+ }
325
+
326
+ .status-value {
327
+ background: #333;
328
+ padding: 5px 12px;
329
+ border-radius: 20px;
330
+ font-size: 14px;
331
+ font-weight: bold;
332
+ }
333
+
334
+ .badge {
335
+ padding: 5px 10px;
336
+ border-radius: 20px;
337
+ background: #333;
338
+ }
339
+
340
+ .badge.green { background: #4CAF50; }
341
+ .badge.red { background: #ff4444; }
342
+ .badge.blue { background: #2196F3; }
343
+
344
+ .info-text {
345
+ color: #666;
346
+ font-size: 12px;
347
+ margin-top: 15px;
348
+ border-top: 1px solid #333;
349
+ padding-top: 15px;
350
+ }
351
+ </style>
352
+ </head>
353
+ <body>
354
+ <div class="container">
355
+ <h1>🎮 Pygame + WebSocket Only <small>Zero HTTP API Calls</small></h1>
356
+
357
+ <div class="video-container">
358
+ <canvas id="canvas" width="640" height="480"></canvas>
359
+ <div id="mouseCoords" class="mouse-coords">X: 320, Y: 240</div>
360
+ </div>
361
+
362
+ <div class="controls">
363
+ <h3>🔊 Sound Source</h3>
364
+ <div class="sound-buttons">
365
+ <button id="btnNone" onclick="setSound('none')" class="active">🔇 None</button>
366
+ <button id="btnPygame" onclick="setSound('pygame')">🎮 Pygame</button>
367
+ <button id="btnBrowser" onclick="setSound('browser')">🌐 Browser</button>
368
+ </div>
369
+
370
+ <div class="status-panel">
371
+ <div class="status-item">
372
+ <span class="status-label">WebSocket:</span>
373
+ <span id="wsStatus" class="status-value">🟢 Connected</span>
374
+ </div>
375
+ <div class="status-item">
376
+ <span class="status-label">Button:</span>
377
+ <span id="buttonState" class="status-value">⚪ Off</span>
378
+ </div>
379
+ <div class="status-item">
380
+ <span class="status-label">Frames:</span>
381
+ <span id="frameCounter" class="status-value">0</span>
382
+ </div>
383
+ </div>
384
+
385
+ <div class="info-text">
386
+ ⚡ Single WebSocket connection • Video + Mouse + Clicks • Zero polling • 30fps
387
+ </div>
388
+ </div>
389
+ </div>
390
+
391
+ <audio id="browserAudio" loop style="display:none;">
392
+ <source src="/static/sound.mp3" type="audio/mpeg">
393
+ </audio>
394
+
395
+ <script>
396
+ const canvas = document.getElementById('canvas');
397
+ const ctx = canvas.getContext('2d');
398
+ const browserAudio = document.getElementById('browserAudio');
399
+
400
+ let frameCount = 0;
401
+ let lastFrameTime = performance.now();
402
+ let fps = 0;
403
+
404
+ // WebSocket connection
405
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/ws');
406
+ ws.binaryType = 'arraybuffer';
407
+
408
+ // Connection status
409
+ ws.onopen = () => {
410
+ document.getElementById('wsStatus').innerHTML = '🟢 Connected';
411
+ document.getElementById('wsStatus').className = 'status-value';
412
+ };
413
+
414
+ ws.onclose = () => {
415
+ document.getElementById('wsStatus').innerHTML = '🔴 Disconnected';
416
+ document.getElementById('wsStatus').className = 'status-value';
417
+ };
418
+
419
+ // Handle incoming messages (video frames + state updates)
420
+ ws.onmessage = (event) => {
421
+ if (event.data instanceof ArrayBuffer) {
422
+ // Video frame
423
+ const data = new Uint8Array(event.data);
424
+ const header = new TextDecoder().decode(data.slice(0, 6));
425
+
426
+ if (header === 'FRAME:') {
427
+ // Extract JPEG data
428
+ const jpegData = data.slice(6);
429
+
430
+ // Convert to blob and create image
431
+ const blob = new Blob([jpegData], {type: 'image/jpeg'});
432
+ const url = URL.createObjectURL(blob);
433
+
434
+ const img = new Image();
435
+ img.onload = () => {
436
+ ctx.drawImage(img, 0, 0, 640, 480);
437
+ URL.revokeObjectURL(url);
438
+
439
+ // Update frame counter
440
+ frameCount++;
441
+ document.getElementById('frameCounter').innerHTML = frameCount;
442
+
443
+ // Calculate FPS occasionally
444
+ const now = performance.now();
445
+ if (now - lastFrameTime > 1000) {
446
+ fps = frameCount;
447
+ frameCount = 0;
448
+ lastFrameTime = now;
449
+ }
450
+ };
451
+ img.src = url;
452
+ }
453
+ } else {
454
+ // JSON message (state updates)
455
+ try {
456
+ const data = JSON.parse(event.data);
457
+ if (data.type === 'state') {
458
+ // Update button state from server
459
+ document.getElementById('buttonState').innerHTML =
460
+ data.button ? '✅ Clicked' : '⚪ Off';
461
+ }
462
+ } catch (e) {
463
+ console.log('Received:', event.data);
464
+ }
465
+ }
466
+ };
467
+
468
+ // Mouse tracking (throttled)
469
+ let mouseTimer;
470
+ canvas.addEventListener('mousemove', (e) => {
471
+ const rect = canvas.getBoundingClientRect();
472
+ const scaleX = canvas.width / rect.width;
473
+ const scaleY = canvas.height / rect.height;
474
+
475
+ const x = Math.round((e.clientX - rect.left) * scaleX);
476
+ const y = Math.round((e.clientY - rect.top) * scaleY);
477
+
478
+ // Clamp to canvas boundaries
479
+ const clampedX = Math.max(0, Math.min(640, x));
480
+ const clampedY = Math.max(0, Math.min(480, y));
481
+
482
+ document.getElementById('mouseCoords').innerHTML = `X: ${clampedX}, Y: ${clampedY}`;
483
+
484
+ // Throttle to 30fps
485
+ if (mouseTimer) clearTimeout(mouseTimer);
486
+ mouseTimer = setTimeout(() => {
487
+ if (ws.readyState === WebSocket.OPEN) {
488
+ ws.send(JSON.stringify({
489
+ type: 'mouse',
490
+ x: clampedX,
491
+ y: clampedY
492
+ }));
493
+ }
494
+ }, 33);
495
+ });
496
+
497
+ // Click handling
498
+ canvas.addEventListener('click', (e) => {
499
+ const rect = canvas.getBoundingClientRect();
500
+ const scaleX = canvas.width / rect.width;
501
+ const scaleY = canvas.height / rect.height;
502
+
503
+ const x = Math.round((e.clientX - rect.left) * scaleX);
504
+ const y = Math.round((e.clientY - rect.top) * scaleY);
505
+
506
+ if (ws.readyState === WebSocket.OPEN) {
507
+ ws.send(JSON.stringify({
508
+ type: 'click',
509
+ x: x,
510
+ y: y
511
+ }));
512
+ }
513
+ });
514
+
515
+ // Sound handling
516
+ function setSound(source) {
517
+ // Update UI
518
+ document.getElementById('btnNone').className = source === 'none' ? 'active' : '';
519
+ document.getElementById('btnPygame').className = source === 'pygame' ? 'active' : '';
520
+ document.getElementById('btnBrowser').className = source === 'browser' ? 'active' : '';
521
+
522
+ // Handle browser audio
523
+ if (source === 'browser') {
524
+ browserAudio.play().catch(e => console.log('Audio error:', e));
525
+ } else {
526
+ browserAudio.pause();
527
+ browserAudio.currentTime = 0;
528
+ }
529
+
530
+ // Send to server
531
+ if (ws.readyState === WebSocket.OPEN) {
532
+ ws.send(JSON.stringify({
533
+ type: 'sound',
534
+ source: source
535
+ }));
536
+ }
537
+ }
538
+
539
+ // Clean up on page unload
540
+ window.addEventListener('beforeunload', () => {
541
+ ws.close();
542
+ });
543
+ </script>
544
+ </body>
545
+ </html>
546
+ ''')
547
+
548
+ @app.route('/static/sound.mp3')
549
+ def serve_sound():
550
+ """Serve sound file for browser playback"""
551
+ if os.path.exists('sound.mp3'):
552
+ with open('sound.mp3', 'rb') as f:
553
+ return Response(f.read(), mimetype='audio/mpeg')
554
+ return 'Sound not found', 404
555
+
556
+ if __name__ == '__main__':
557
+ print("\n" + "="*70)
558
+ print("🎮 Pygame + WebSocket Only")
559
+ print("="*70)
560
+ print("🔌 Single WebSocket for everything:")
561
+ print(" • Video streaming (30fps JPEG)")
562
+ print(" • Mouse tracking")
563
+ print(" • Click handling")
564
+ print(" • Sound control")
565
+ print("📡 Zero HTTP API calls")
566
+ print("🖱️ Interactive button inside Pygame")
567
+ print("⏱️ Live clock display")
568
+ print("\n🌐 Main page: /")
569
+ print("="*70 + "\n")
570
+
571
+ port = int(os.environ.get('PORT', 7860))
572
+ app.run(host='0.0.0.0', port=port, debug=False, threaded=True)