MySafeCode commited on
Commit
15e243b
·
verified ·
1 Parent(s): 1c73829

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -492
app.py CHANGED
@@ -1,57 +1,28 @@
1
  import pygame
2
  import numpy as np
3
- from flask import Flask, Response, render_template_string, request, jsonify
 
4
  import time
5
  import os
6
  import cv2
7
- import subprocess
8
  import threading
9
- import queue
10
- import uuid
11
-
12
- # ============ CONFIGURATION ============
13
- VIDEO_WIDTH = 1280
14
- VIDEO_HEIGHT = 720
15
- VIDEO_FPS = 30
16
- JPEG_QUALITY = 70
17
- STREAM_PORT = 7860
18
-
19
- # Colors (RGB)
20
- COLOR_TOP = (255, 100, 100)
21
- COLOR_BOTTOM = (100, 255, 100)
22
- COLOR_BG = (20, 20, 30)
23
- COLOR_GRID = (50, 50, 70)
24
- COLOR_FPS = (255, 255, 0)
25
- COLOR_CLOCK = (0, 255, 255)
26
-
27
- # Button colors
28
- COLOR_BUTTON_NORMAL = (80, 80, 80)
29
- COLOR_BUTTON_HOVER = (100, 100, 200)
30
- COLOR_BUTTON_CLICKED = (0, 200, 0)
31
- COLOR_BUTTON_BORDER = (200, 200, 200)
32
-
33
- # Sound source colors
34
- COLOR_SOUND_NONE = (255, 100, 100)
35
- COLOR_SOUND_PYGAME = (100, 255, 100)
36
- COLOR_SOUND_BROWSER = (100, 100, 255)
37
- # =======================================
38
 
39
  # Initialize Pygame headlessly
40
  os.environ['SDL_VIDEODRIVER'] = 'dummy'
41
  pygame.init()
42
 
43
- # Try to initialize mixer, but don't crash if it fails
44
  try:
45
  pygame.mixer.init(frequency=44100, size=-16, channels=2)
46
  print("✅ Audio mixer initialized")
47
  except Exception as e:
48
  print(f"⚠️ Audio mixer not available: {e}")
49
- print(" Continuing without sound...")
50
 
51
  app = Flask(__name__)
 
52
 
53
  class ShaderRenderer:
54
- def __init__(self, width=VIDEO_WIDTH, height=VIDEO_HEIGHT):
55
  self.width = width
56
  self.height = height
57
  self.mouse_x = width // 2
@@ -62,53 +33,20 @@ class ShaderRenderer:
62
  self.last_frame_time = time.time()
63
  self.fps = 0
64
  self.button_clicked = False
65
-
66
- # Sound sources
67
  self.sound_source = 'none'
68
- self.pygame_sound = None
69
- self.pygame_playing = False
70
- self.sound_amp = 0.0
71
 
72
- # Load pygame sound if available
73
- if os.path.exists('sound.mp3'):
74
- try:
75
- self.pygame_sound = pygame.mixer.Sound('sound.mp3')
76
- print("✅ Pygame sound loaded")
77
- except:
78
- print("⚠️ Could not load sound.mp3")
79
-
80
  def set_mouse(self, x, y):
81
  self.mouse_x = max(0, min(self.width, x))
82
  self.mouse_y = max(0, min(self.height, y))
83
 
84
- def set_sound_source(self, source):
85
- """Change sound source: none, pygame, browser"""
86
- self.sound_source = source
87
-
88
- # Handle pygame sound
89
- if source == 'pygame':
90
- if self.pygame_sound and not self.pygame_playing:
91
- self.pygame_sound.play(loops=-1)
92
- self.pygame_playing = True
93
- self.sound_amp = 0.5
94
- else:
95
- if self.pygame_playing:
96
- pygame.mixer.stop()
97
- self.pygame_playing = False
98
- self.sound_amp = 0.0
99
-
100
  def handle_click(self, x, y):
101
- """Handle mouse clicks on Pygame surface"""
102
- button_rect = pygame.Rect(self.width-250, 120, 220, 50)
103
-
104
  if button_rect.collidepoint(x, y):
105
  self.button_clicked = not self.button_clicked
106
- print(f"🎯 Button {'clicked!' if self.button_clicked else 'unclicked!'}")
107
  return True
108
  return False
109
 
110
  def render_frame(self):
111
- """Render the pygame frame"""
112
  t = time.time() - self.start_time
113
 
114
  # Calculate FPS
@@ -119,274 +57,132 @@ class ShaderRenderer:
119
  self.last_frame_time = time.time()
120
 
121
  # Clear
122
- self.surface.fill(COLOR_BG)
123
-
124
- # Use larger font for 720p
125
- font = pygame.font.Font(None, 36)
126
- small_font = pygame.font.Font(None, 24)
127
 
128
  # Draw TOP marker
129
- pygame.draw.rect(self.surface, COLOR_TOP, (10, 10, 150, 40))
130
  text = font.render("TOP", True, (255, 255, 255))
131
- self.surface.blit(text, (30, 15))
132
 
133
  # Draw BOTTOM marker
134
- pygame.draw.rect(self.surface, COLOR_BOTTOM, (10, self.height-50, 150, 40))
135
  text = font.render("BOTTOM", True, (0, 0, 0))
136
- self.surface.blit(text, (20, self.height-45))
137
 
138
- # ===== CLOCK DISPLAY =====
139
  current_time = time.time()
140
  seconds = int(current_time) % 60
141
  hundredths = int((current_time * 100) % 100)
142
-
143
- # Clock background
144
- pygame.draw.rect(self.surface, (40, 40, 50), (self.width-250, 70, 220, 50))
145
- pygame.draw.rect(self.surface, (100, 100, 150), (self.width-250, 70, 220, 50), 2)
146
-
147
- # Clock text
148
  time_str = f"{seconds:02d}.{hundredths:02d}s"
149
- clock_text = font.render(time_str, True, COLOR_CLOCK)
150
- self.surface.blit(clock_text, (self.width-230, 80))
151
-
152
- # ===== CLICKABLE BUTTON =====
153
- button_rect = pygame.Rect(self.width-250, 140, 220, 50)
154
 
155
- # Check if mouse is over button
 
156
  mouse_over = button_rect.collidepoint(self.mouse_x, self.mouse_y)
157
 
158
- # Button color based on state and hover
159
  if self.button_clicked:
160
- button_color = COLOR_BUTTON_CLICKED
161
  elif mouse_over:
162
- button_color = COLOR_BUTTON_HOVER
163
  else:
164
- button_color = COLOR_BUTTON_NORMAL
165
-
166
- # Draw button
167
  pygame.draw.rect(self.surface, button_color, button_rect)
168
- pygame.draw.rect(self.surface, COLOR_BUTTON_BORDER, button_rect, 3)
169
-
170
- # Button text
171
- if self.button_clicked:
172
- btn_text = "✅ CLICKED!"
173
- else:
174
- btn_text = "🔘 CLICK ME"
175
 
 
176
  text_surf = font.render(btn_text, True, (255, 255, 255))
177
  text_rect = text_surf.get_rect(center=button_rect.center)
178
  self.surface.blit(text_surf, text_rect)
179
 
180
- # ===== MOVING CIRCLE =====
181
- circle_size = 40 + int(30 * np.sin(t * 2))
182
-
183
- # Color code based on sound source
184
  if self.sound_source == 'pygame':
185
- color = COLOR_SOUND_PYGAME
186
  elif self.sound_source == 'browser':
187
- color = COLOR_SOUND_BROWSER
188
  else:
189
- color = COLOR_SOUND_NONE
190
 
191
  pygame.draw.circle(self.surface, color,
192
  (self.mouse_x, self.mouse_y), circle_size)
193
 
194
- # ===== GRID =====
195
- for x in range(0, self.width, 70):
196
  alpha = int(40 + 20 * np.sin(x * 0.1 + t))
197
  pygame.draw.line(self.surface, (alpha, alpha, 50),
198
  (x, 0), (x, self.height))
199
- for y in range(0, self.height, 70):
200
  alpha = int(40 + 20 * np.cos(y * 0.1 + t))
201
  pygame.draw.line(self.surface, (alpha, alpha, 50),
202
  (0, y), (self.width, y))
203
 
204
- # ===== SOUND METER =====
205
- meter_width = int(250 * self.sound_amp)
206
- pygame.draw.rect(self.surface, (60, 60, 60), (self.width-270, 210, 250, 25))
207
- pygame.draw.rect(self.surface, (100, 255, 100),
208
- (self.width-270, 210, meter_width, 25))
209
-
210
- # ===== FPS COUNTER =====
211
- fps_text = small_font.render(f"FPS: {self.fps}", True, COLOR_FPS)
212
- self.surface.blit(fps_text, (self.width-200, self.height-80))
213
 
214
  return pygame.image.tostring(self.surface, 'RGB')
215
 
216
- def get_frame(self):
217
- return self.render_frame()
218
-
219
- def get_frame_jpeg(self, quality=JPEG_QUALITY):
220
- """Return frame as JPEG"""
221
- frame = self.get_frame()
222
- # Convert to numpy array for OpenCV
223
  img = np.frombuffer(frame, dtype=np.uint8).reshape((self.height, self.width, 3))
224
- # Convert RGB to BGR for OpenCV
225
  img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
226
- # Encode as JPEG
227
  _, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, quality])
228
  return jpeg.tobytes()
229
 
230
  renderer = ShaderRenderer()
231
 
232
- # ============ STREAMING ENDPOINTS ============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
- @app.route('/video/mjpeg')
 
235
  def video_mjpeg():
236
- """MJPEG streaming endpoint"""
237
  def generate():
238
  while True:
239
  frame = renderer.get_frame_jpeg()
240
  yield (b'--frame\r\n'
241
  b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
242
- time.sleep(1/VIDEO_FPS)
243
 
244
  return Response(
245
  generate(),
246
  mimetype='multipart/x-mixed-replace; boundary=frame'
247
  )
248
 
249
- @app.route('/video/webm')
250
- def video_webm():
251
- """WebM streaming endpoint"""
252
- cmd = [
253
- 'ffmpeg',
254
- '-f', 'rawvideo',
255
- '-pix_fmt', 'rgb24',
256
- '-s', f'{VIDEO_WIDTH}x{VIDEO_HEIGHT}',
257
- '-r', str(VIDEO_FPS),
258
- '-i', '-',
259
- '-c:v', 'libvpx-vp9',
260
- '-b:v', '2M',
261
- '-cpu-used', '4',
262
- '-deadline', 'realtime',
263
- '-f', 'webm',
264
- '-'
265
- ]
266
-
267
- process = subprocess.Popen(
268
- cmd,
269
- stdin=subprocess.PIPE,
270
- stdout=subprocess.PIPE,
271
- stderr=subprocess.DEVNULL,
272
- bufsize=0
273
- )
274
-
275
- def generate():
276
- def push_frames():
277
- while True:
278
- try:
279
- frame = renderer.get_frame()
280
- process.stdin.write(frame)
281
- except:
282
- break
283
-
284
- threading.Thread(target=push_frames, daemon=True).start()
285
-
286
- while True:
287
- data = process.stdout.read(4096)
288
- if not data:
289
- break
290
- yield data
291
-
292
- return Response(
293
- generate(),
294
- mimetype='video/webm',
295
- headers={'Cache-Control': 'no-cache', 'Transfer-Encoding': 'chunked'}
296
- )
297
-
298
- @app.route('/video/mp4')
299
- def video_mp4():
300
- """MP4 streaming endpoint"""
301
- cmd = [
302
- 'ffmpeg',
303
- '-f', 'rawvideo',
304
- '-pix_fmt', 'rgb24',
305
- '-s', f'{VIDEO_WIDTH}x{VIDEO_HEIGHT}',
306
- '-r', str(VIDEO_FPS),
307
- '-i', '-',
308
- '-c:v', 'libx264',
309
- '-preset', 'ultrafast',
310
- '-tune', 'zerolatency',
311
- '-b:v', '2M',
312
- '-f', 'mp4',
313
- '-movflags', 'frag_keyframe+empty_moov',
314
- '-'
315
- ]
316
-
317
- process = subprocess.Popen(
318
- cmd,
319
- stdin=subprocess.PIPE,
320
- stdout=subprocess.PIPE,
321
- stderr=subprocess.DEVNULL,
322
- bufsize=0
323
- )
324
-
325
- def generate():
326
- def push_frames():
327
- while True:
328
- try:
329
- frame = renderer.get_frame()
330
- process.stdin.write(frame)
331
- except:
332
- break
333
-
334
- threading.Thread(target=push_frames, daemon=True).start()
335
-
336
- while True:
337
- data = process.stdout.read(4096)
338
- if not data:
339
- break
340
- yield data
341
-
342
- return Response(
343
- generate(),
344
- mimetype='video/mp4',
345
- headers={'Cache-Control': 'no-cache', 'Transfer-Encoding': 'chunked'}
346
- )
347
-
348
- # ============ INTERACTIVITY ENDPOINTS ============
349
-
350
- @app.route('/mouse', methods=['POST'])
351
- def mouse():
352
- data = request.json
353
- renderer.set_mouse(data['x'], data['y'])
354
- return 'OK'
355
-
356
- @app.route('/click', methods=['POST'])
357
- def click():
358
- data = request.json
359
- renderer.handle_click(data['x'], data['y'])
360
- return 'OK'
361
-
362
- @app.route('/sound/source', methods=['POST'])
363
- def sound_source():
364
- data = request.json
365
- renderer.set_sound_source(data['source'])
366
- return 'OK'
367
-
368
- @app.route('/sound/amp')
369
- def sound_amp():
370
- return {'amp': renderer.sound_amp}
371
-
372
- @app.route('/static/sound.mp3')
373
- def serve_sound():
374
- if os.path.exists('sound.mp3'):
375
- with open('sound.mp3', 'rb') as f:
376
- return Response(f.read(), mimetype='audio/mpeg')
377
- return 'Sound not found', 404
378
-
379
- # ============ HTML PAGE ============
380
-
381
  @app.route('/')
382
  def index():
383
- return render_template_string(f'''
384
  <!DOCTYPE html>
385
  <html>
386
  <head>
387
- <title>🎮 Pygame 720p Streaming</title>
388
  <style>
389
- body {{
390
  margin: 0;
391
  background: #0a0a0a;
392
  color: white;
@@ -395,44 +191,28 @@ def index():
395
  justify-content: center;
396
  align-items: center;
397
  min-height: 100vh;
398
- }}
399
-
400
- .container {{
401
- max-width: 1400px;
402
  padding: 20px;
403
  text-align: center;
404
- }}
405
-
406
- h1 {{
407
- color: #4CAF50;
408
- margin-bottom: 20px;
409
- }}
410
-
411
- .video-container {{
412
  background: #000;
413
  border-radius: 12px;
414
  padding: 5px;
415
  margin: 20px 0;
416
- box-shadow: 0 0 30px rgba(76, 175, 80, 0.2);
417
  position: relative;
418
- }}
419
-
420
- #videoPlayer, #mjpegImg {{
421
  width: 100%;
422
- max-width: {VIDEO_WIDTH}px;
423
  height: auto;
424
  border-radius: 8px;
425
- display: block;
426
- margin: 0 auto;
427
- background: #111;
428
  cursor: crosshair;
429
- }}
430
-
431
- #mjpegImg {{
432
- display: none;
433
- }}
434
-
435
- .mouse-coords {{
436
  position: absolute;
437
  bottom: 10px;
438
  left: 10px;
@@ -441,138 +221,73 @@ def index():
441
  padding: 5px 10px;
442
  border-radius: 20px;
443
  font-family: monospace;
444
- font-size: 14px;
445
- pointer-events: none;
446
- }}
447
-
448
- .controls {{
449
  background: #1a1a1a;
450
  border-radius: 12px;
451
  padding: 20px;
452
  margin-top: 20px;
453
- }}
454
-
455
- .format-buttons, .sound-controls {{
456
  display: flex;
457
  gap: 10px;
458
  justify-content: center;
459
  margin: 20px 0;
460
- flex-wrap: wrap;
461
- }}
462
-
463
- button {{
464
  background: #333;
465
  color: white;
466
  border: none;
467
  padding: 12px 24px;
468
- font-size: 16px;
469
  border-radius: 8px;
470
  cursor: pointer;
471
- transition: all 0.3s;
472
- min-width: 120px;
473
  font-weight: bold;
474
- border: 1px solid #444;
475
- }}
476
-
477
- button:hover {{
478
- transform: translateY(-2px);
479
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
480
- }}
481
-
482
- button.active {{
483
  background: #4CAF50;
484
- border-color: #4CAF50;
485
  box-shadow: 0 0 20px #4CAF50;
486
- }}
487
-
488
- .status-panel {{
489
- background: #222;
490
- border-radius: 8px;
491
- padding: 15px;
492
- margin-top: 20px;
493
  display: flex;
494
  justify-content: space-around;
495
- flex-wrap: wrap;
496
- gap: 15px;
497
- }}
498
-
499
- .status-item {{
500
- display: flex;
501
- align-items: center;
502
- gap: 10px;
503
- }}
504
-
505
- .status-label {{
506
- color: #888;
507
- font-size: 14px;
508
- }}
509
-
510
- .status-value {{
511
- background: #333;
512
- padding: 5px 12px;
513
  border-radius: 20px;
514
- font-size: 14px;
515
- font-weight: bold;
516
- }}
517
-
518
- .meter {{
519
- width: 100%;
520
- height: 25px;
521
  background: #333;
522
- border-radius: 12px;
523
- overflow: hidden;
524
- margin: 10px 0;
525
- }}
526
-
527
- .meter-fill {{
528
- height: 100%;
529
- width: 0%;
530
- background: linear-gradient(90deg, #4CAF50, #2196F3);
531
- transition: width 0.05s;
532
- }}
533
  </style>
534
  </head>
535
  <body>
536
  <div class="container">
537
- <h1>🎮 Pygame 720p Streaming</h1>
538
 
539
  <div class="video-container">
540
- <video id="videoPlayer" autoplay controls muted></video>
541
- <img id="mjpegImg" crossorigin="anonymous">
542
  <div id="mouseCoords" class="mouse-coords">X: 320, Y: 240</div>
543
  </div>
544
 
545
  <div class="controls">
546
- <div class="format-buttons">
547
- <button id="btnMjpeg" onclick="setFormat('mjpeg')" class="active">📸 MJPEG</button>
548
- <button id="btnWebm" onclick="setFormat('webm')">🎥 WebM</button>
549
- <button id="btnMp4" onclick="setFormat('mp4')">🎬 MP4</button>
550
- </div>
551
-
552
- <div class="sound-controls">
553
  <button id="btnNone" onclick="setSound('none')" class="active">🔇 None</button>
554
  <button id="btnPygame" onclick="setSound('pygame')">🎮 Pygame</button>
555
  <button id="btnBrowser" onclick="setSound('browser')">🌐 Browser</button>
556
  </div>
557
 
558
- <div class="meter">
559
- <div id="soundMeter" class="meter-fill"></div>
 
560
  </div>
561
 
562
- <div class="status-panel">
563
- <div class="status-item">
564
- <span class="status-label">Format:</span>
565
- <span id="currentFormat" class="status-value">MJPEG</span>
566
- </div>
567
- <div class="status-item">
568
- <span class="status-label">Sound:</span>
569
- <span id="currentSource" class="status-value">None</span>
570
- </div>
571
- <div class="status-item">
572
- <span class="status-label">Resolution:</span>
573
- <span class="status-value">{VIDEO_WIDTH}x{VIDEO_HEIGHT}</span>
574
- </div>
575
- </div>
576
  </div>
577
  </div>
578
 
@@ -581,128 +296,78 @@ def index():
581
  </audio>
582
 
583
  <script>
584
- const videoPlayer = document.getElementById('videoPlayer');
585
- const mjpegImg = document.getElementById('mjpegImg');
586
  const browserAudio = document.getElementById('browserAudio');
587
 
588
- let currentFormat = 'mjpeg';
589
- let currentSource = 'none';
590
- let lastMouseSend = 0;
591
 
592
- // Mouse tracking
593
- function handleMouseMove(e) {{
594
- const rect = (currentFormat === 'mjpeg' ? mjpegImg : videoPlayer).getBoundingClientRect();
595
- const x = Math.round((e.clientX - rect.left) * {VIDEO_WIDTH} / rect.width);
596
- const y = Math.round((e.clientY - rect.top) * {VIDEO_HEIGHT} / rect.height);
 
 
 
 
597
 
598
- document.getElementById('mouseCoords').innerHTML = `X: ${{x}}, Y: ${{y}}`;
599
 
600
- const now = Date.now();
601
- if (now - lastMouseSend > 33) {{
602
- fetch('/mouse', {{
603
- method: 'POST',
604
- headers: {{'Content-Type': 'application/json'}},
605
- body: JSON.stringify({{x: x, y: y}})
606
- }});
607
- lastMouseSend = now;
608
- }}
609
- }}
610
 
611
- // Click handling for button
612
- function handleClick(e) {{
613
- const rect = (currentFormat === 'mjpeg' ? mjpegImg : videoPlayer).getBoundingClientRect();
614
- const x = Math.round((e.clientX - rect.left) * {VIDEO_WIDTH} / rect.width);
615
- const y = Math.round((e.clientY - rect.top) * {VIDEO_HEIGHT} / rect.height);
616
 
617
- fetch('/click', {{
618
- method: 'POST',
619
- headers: {{'Content-Type': 'application/json'}},
620
- body: JSON.stringify({{x: x, y: y}})
621
- }});
622
- }}
623
-
624
- videoPlayer.addEventListener('mousemove', handleMouseMove);
625
- mjpegImg.addEventListener('mousemove', handleMouseMove);
626
- videoPlayer.addEventListener('click', handleClick);
627
- mjpegImg.addEventListener('click', handleClick);
628
 
629
  // Sound handling
630
- function setSound(source) {{
631
- currentSource = source;
632
-
633
  document.getElementById('btnNone').className = source === 'none' ? 'active' : '';
634
  document.getElementById('btnPygame').className = source === 'pygame' ? 'active' : '';
635
  document.getElementById('btnBrowser').className = source === 'browser' ? 'active' : '';
636
- document.getElementById('currentSource').innerHTML =
637
- source.charAt(0).toUpperCase() + source.slice(1);
638
 
639
- if (source === 'browser') {{
640
  browserAudio.play().catch(e => console.log('Audio error:', e));
641
- }} else {{
642
  browserAudio.pause();
643
  browserAudio.currentTime = 0;
644
- }}
645
 
646
- fetch('/sound/source', {{
647
- method: 'POST',
648
- headers: {{'Content-Type': 'application/json'}},
649
- body: JSON.stringify({{source: source}})
650
- }});
651
- }}
652
-
653
- // Sound meter
654
- function updateSoundMeter() {{
655
- fetch('/sound/amp')
656
- .then(res => res.json())
657
- .then(data => {{
658
- document.getElementById('soundMeter').style.width = (data.amp * 100) + '%';
659
- }});
660
- setTimeout(updateSoundMeter, 100);
661
- }}
662
- updateSoundMeter();
663
-
664
- // Format switching
665
- function setFormat(format) {{
666
- currentFormat = format;
667
-
668
- document.getElementById('btnMjpeg').className = format === 'mjpeg' ? 'active' : '';
669
- document.getElementById('btnWebm').className = format === 'webm' ? 'active' : '';
670
- document.getElementById('btnMp4').className = format === 'mp4' ? 'active' : '';
671
- document.getElementById('currentFormat').innerHTML = format.toUpperCase();
672
-
673
- videoPlayer.style.display = 'none';
674
- mjpegImg.style.display = 'none';
675
-
676
- if (format === 'mjpeg') {{
677
- mjpegImg.style.display = 'block';
678
- mjpegImg.src = '/video/mjpeg?' + Date.now();
679
- }} else {{
680
- videoPlayer.style.display = 'block';
681
- videoPlayer.src = `/video/${{format}}?` + Date.now();
682
- videoPlayer.play().catch(e => console.log('Playback error:', e));
683
- }}
684
- }}
685
-
686
- // Initialize
687
- setFormat('mjpeg');
688
- setSound('none');
689
  </script>
690
  </body>
691
  </html>
692
  ''')
693
 
 
 
 
 
 
 
 
694
  if __name__ == '__main__':
695
- print("\n" + "="*70)
696
- print("🎮 Pygame 720p Streaming App")
697
- print("="*70)
698
- print(f"📡 Resolution: {VIDEO_WIDTH}x{VIDEO_HEIGHT} @ {VIDEO_FPS}fps")
699
- print("📡 Streaming endpoints:")
700
- print(" • /video/mjpeg - MJPEG stream")
701
- print(" • /video/webm - WebM stream")
702
- print(" • /video/mp4 - MP4 stream")
703
- print("🖱️ Interactive: mouse + clickable button")
704
- print(f"\n🌐 Main page: /")
705
- print("="*70 + "\n")
706
-
707
- port = int(os.environ.get('PORT', STREAM_PORT))
708
  app.run(host='0.0.0.0', port=port, debug=False, threaded=True)
 
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
 
11
  # Initialize Pygame headlessly
12
  os.environ['SDL_VIDEODRIVER'] = 'dummy'
13
  pygame.init()
14
 
 
15
  try:
16
  pygame.mixer.init(frequency=44100, size=-16, channels=2)
17
  print("✅ Audio mixer initialized")
18
  except Exception as e:
19
  print(f"⚠️ Audio mixer not available: {e}")
 
20
 
21
  app = Flask(__name__)
22
+ sock = Sock(app)
23
 
24
  class ShaderRenderer:
25
+ def __init__(self, width=640, height=480):
26
  self.width = width
27
  self.height = height
28
  self.mouse_x = width // 2
 
33
  self.last_frame_time = time.time()
34
  self.fps = 0
35
  self.button_clicked = False
 
 
36
  self.sound_source = 'none'
 
 
 
37
 
 
 
 
 
 
 
 
 
38
  def set_mouse(self, x, y):
39
  self.mouse_x = max(0, min(self.width, x))
40
  self.mouse_y = max(0, min(self.height, y))
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def handle_click(self, x, y):
43
+ button_rect = pygame.Rect(self.width-200, 120, 180, 40)
 
 
44
  if button_rect.collidepoint(x, y):
45
  self.button_clicked = not self.button_clicked
 
46
  return True
47
  return False
48
 
49
  def render_frame(self):
 
50
  t = time.time() - self.start_time
51
 
52
  # Calculate FPS
 
57
  self.last_frame_time = time.time()
58
 
59
  # Clear
60
+ self.surface.fill((20, 20, 30))
61
+ font = pygame.font.Font(None, 24)
 
 
 
62
 
63
  # Draw TOP marker
64
+ pygame.draw.rect(self.surface, (255, 100, 100), (10, 10, 100, 30))
65
  text = font.render("TOP", True, (255, 255, 255))
66
+ self.surface.blit(text, (20, 15))
67
 
68
  # Draw BOTTOM marker
69
+ pygame.draw.rect(self.surface, (100, 255, 100), (10, self.height-40, 100, 30))
70
  text = font.render("BOTTOM", True, (0, 0, 0))
71
+ self.surface.blit(text, (20, self.height-35))
72
 
73
+ # Draw CLOCK
74
  current_time = time.time()
75
  seconds = int(current_time) % 60
76
  hundredths = int((current_time * 100) % 100)
 
 
 
 
 
 
77
  time_str = f"{seconds:02d}.{hundredths:02d}s"
78
+ clock_text = font.render(time_str, True, (0, 255, 255))
79
+ self.surface.blit(clock_text, (self.width-150, 40))
 
 
 
80
 
81
+ # Draw BUTTON
82
+ button_rect = pygame.Rect(self.width-200, 120, 180, 40)
83
  mouse_over = button_rect.collidepoint(self.mouse_x, self.mouse_y)
84
 
 
85
  if self.button_clicked:
86
+ button_color = (0, 200, 0)
87
  elif mouse_over:
88
+ button_color = (100, 100, 200)
89
  else:
90
+ button_color = (80, 80, 80)
91
+
 
92
  pygame.draw.rect(self.surface, button_color, button_rect)
93
+ pygame.draw.rect(self.surface, (200, 200, 200), button_rect, 2)
 
 
 
 
 
 
94
 
95
+ btn_text = "✅ CLICKED!" if self.button_clicked else "🔘 CLICK ME"
96
  text_surf = font.render(btn_text, True, (255, 255, 255))
97
  text_rect = text_surf.get_rect(center=button_rect.center)
98
  self.surface.blit(text_surf, text_rect)
99
 
100
+ # Draw circle
101
+ circle_size = 30 + int(20 * np.sin(t * 2))
 
 
102
  if self.sound_source == 'pygame':
103
+ color = (100, 255, 100)
104
  elif self.sound_source == 'browser':
105
+ color = (100, 100, 255)
106
  else:
107
+ color = (255, 100, 100)
108
 
109
  pygame.draw.circle(self.surface, color,
110
  (self.mouse_x, self.mouse_y), circle_size)
111
 
112
+ # Draw grid
113
+ for x in range(0, self.width, 50):
114
  alpha = int(40 + 20 * np.sin(x * 0.1 + t))
115
  pygame.draw.line(self.surface, (alpha, alpha, 50),
116
  (x, 0), (x, self.height))
117
+ for y in range(0, self.height, 50):
118
  alpha = int(40 + 20 * np.cos(y * 0.1 + t))
119
  pygame.draw.line(self.surface, (alpha, alpha, 50),
120
  (0, y), (self.width, y))
121
 
122
+ # FPS counter
123
+ fps_text = font.render(f"FPS: {self.fps}", True, (255, 255, 0))
124
+ self.surface.blit(fps_text, (self.width-150, self.height-60))
 
 
 
 
 
 
125
 
126
  return pygame.image.tostring(self.surface, 'RGB')
127
 
128
+ def get_frame_jpeg(self, quality=70):
129
+ frame = self.render_frame()
 
 
 
 
 
130
  img = np.frombuffer(frame, dtype=np.uint8).reshape((self.height, self.width, 3))
 
131
  img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
 
132
  _, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, quality])
133
  return jpeg.tobytes()
134
 
135
  renderer = ShaderRenderer()
136
 
137
+ # WebSocket for all interactions
138
+ @sock.route('/ws')
139
+ def websocket(ws):
140
+ """Single WebSocket connection for all interaction"""
141
+ while True:
142
+ try:
143
+ message = ws.receive()
144
+ if not message:
145
+ continue
146
+
147
+ data = json.loads(message)
148
+
149
+ if data['type'] == 'mouse':
150
+ renderer.set_mouse(data['x'], data['y'])
151
+
152
+ elif data['type'] == 'click':
153
+ renderer.handle_click(data['x'], data['y'])
154
+ # Broadcast button state to all clients? Optional
155
+
156
+ elif data['type'] == 'sound':
157
+ renderer.sound_source = data['source']
158
+
159
+ except:
160
+ break
161
 
162
+ # MJPEG stream (one long connection, zero API calls)
163
+ @app.route('/video.mjpeg')
164
  def video_mjpeg():
 
165
  def generate():
166
  while True:
167
  frame = renderer.get_frame_jpeg()
168
  yield (b'--frame\r\n'
169
  b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
170
+ time.sleep(1/30)
171
 
172
  return Response(
173
  generate(),
174
  mimetype='multipart/x-mixed-replace; boundary=frame'
175
  )
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  @app.route('/')
178
  def index():
179
+ return render_template_string('''
180
  <!DOCTYPE html>
181
  <html>
182
  <head>
183
+ <title>🎮 Pygame + WebSocket</title>
184
  <style>
185
+ body {
186
  margin: 0;
187
  background: #0a0a0a;
188
  color: white;
 
191
  justify-content: center;
192
  align-items: center;
193
  min-height: 100vh;
194
+ }
195
+ .container {
196
+ max-width: 900px;
 
197
  padding: 20px;
198
  text-align: center;
199
+ }
200
+ h1 { color: #4CAF50; }
201
+ .video-container {
 
 
 
 
 
202
  background: #000;
203
  border-radius: 12px;
204
  padding: 5px;
205
  margin: 20px 0;
 
206
  position: relative;
207
+ }
208
+ #mjpegImg {
 
209
  width: 100%;
210
+ max-width: 640px;
211
  height: auto;
212
  border-radius: 8px;
 
 
 
213
  cursor: crosshair;
214
+ }
215
+ .mouse-coords {
 
 
 
 
 
216
  position: absolute;
217
  bottom: 10px;
218
  left: 10px;
 
221
  padding: 5px 10px;
222
  border-radius: 20px;
223
  font-family: monospace;
224
+ }
225
+ .controls {
 
 
 
226
  background: #1a1a1a;
227
  border-radius: 12px;
228
  padding: 20px;
229
  margin-top: 20px;
230
+ }
231
+ .sound-buttons {
 
232
  display: flex;
233
  gap: 10px;
234
  justify-content: center;
235
  margin: 20px 0;
236
+ }
237
+ button {
 
 
238
  background: #333;
239
  color: white;
240
  border: none;
241
  padding: 12px 24px;
 
242
  border-radius: 8px;
243
  cursor: pointer;
 
 
244
  font-weight: bold;
245
+ }
246
+ button.active {
 
 
 
 
 
 
 
247
  background: #4CAF50;
 
248
  box-shadow: 0 0 20px #4CAF50;
249
+ }
250
+ .status {
 
 
 
 
 
251
  display: flex;
252
  justify-content: space-around;
253
+ margin-top: 15px;
254
+ padding: 10px;
255
+ background: #222;
256
+ border-radius: 8px;
257
+ }
258
+ .badge {
259
+ padding: 5px 10px;
 
 
 
 
 
 
 
 
 
 
 
260
  border-radius: 20px;
 
 
 
 
 
 
 
261
  background: #333;
262
+ }
263
+ .badge.green { background: #4CAF50; }
 
 
 
 
 
 
 
 
 
264
  </style>
265
  </head>
266
  <body>
267
  <div class="container">
268
+ <h1>🎮 Pygame + WebSocket (Zero API)</h1>
269
 
270
  <div class="video-container">
271
+ <img id="mjpegImg" src="/video.mjpeg" crossorigin="anonymous">
 
272
  <div id="mouseCoords" class="mouse-coords">X: 320, Y: 240</div>
273
  </div>
274
 
275
  <div class="controls">
276
+ <h3>🔊 Sound Source</h3>
277
+ <div class="sound-buttons">
 
 
 
 
 
278
  <button id="btnNone" onclick="setSound('none')" class="active">🔇 None</button>
279
  <button id="btnPygame" onclick="setSound('pygame')">🎮 Pygame</button>
280
  <button id="btnBrowser" onclick="setSound('browser')">🌐 Browser</button>
281
  </div>
282
 
283
+ <div class="status">
284
+ <div>Connection: <span id="wsStatus" class="badge green">🟢 Connected</span></div>
285
+ <div>API Calls: <span class="badge">0</span></div>
286
  </div>
287
 
288
+ <p style="color: #666; font-size: 12px; margin-top: 15px;">
289
+ Zero API polling • All interaction via WebSocket • MJPEG stream
290
+ </p>
 
 
 
 
 
 
 
 
 
 
 
291
  </div>
292
  </div>
293
 
 
296
  </audio>
297
 
298
  <script>
299
+ const img = document.getElementById('mjpegImg');
 
300
  const browserAudio = document.getElementById('browserAudio');
301
 
302
+ // WebSocket connection (single socket for everything)
303
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/ws');
 
304
 
305
+ ws.onopen = () => document.getElementById('wsStatus').innerHTML = '🟢 Connected';
306
+ ws.onclose = () => document.getElementById('wsStatus').innerHTML = '🔴 Disconnected';
307
+
308
+ // Mouse tracking - send via WebSocket
309
+ let mouseTimer;
310
+ img.addEventListener('mousemove', (e) => {
311
+ const rect = img.getBoundingClientRect();
312
+ const x = Math.round((e.clientX - rect.left) * 640 / rect.width);
313
+ const y = Math.round((e.clientY - rect.top) * 480 / rect.height);
314
 
315
+ document.getElementById('mouseCoords').innerHTML = `X: ${x}, Y: ${y}`;
316
 
317
+ // Throttle to 30fps
318
+ if (mouseTimer) clearTimeout(mouseTimer);
319
+ mouseTimer = setTimeout(() => {
320
+ ws.send(JSON.stringify({
321
+ type: 'mouse',
322
+ x: x,
323
+ y: y
324
+ }));
325
+ }, 33);
326
+ });
327
 
328
+ // Click handling - send via WebSocket
329
+ img.addEventListener('click', (e) => {
330
+ const rect = img.getBoundingClientRect();
331
+ const x = Math.round((e.clientX - rect.left) * 640 / rect.width);
332
+ const y = Math.round((e.clientY - rect.top) * 480 / rect.height);
333
 
334
+ ws.send(JSON.stringify({
335
+ type: 'click',
336
+ x: x,
337
+ y: y
338
+ }));
339
+ });
 
 
 
 
 
340
 
341
  // Sound handling
342
+ function setSound(source) {
 
 
343
  document.getElementById('btnNone').className = source === 'none' ? 'active' : '';
344
  document.getElementById('btnPygame').className = source === 'pygame' ? 'active' : '';
345
  document.getElementById('btnBrowser').className = source === 'browser' ? 'active' : '';
 
 
346
 
347
+ if (source === 'browser') {
348
  browserAudio.play().catch(e => console.log('Audio error:', e));
349
+ } else {
350
  browserAudio.pause();
351
  browserAudio.currentTime = 0;
352
+ }
353
 
354
+ ws.send(JSON.stringify({
355
+ type: 'sound',
356
+ source: source
357
+ }));
358
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  </script>
360
  </body>
361
  </html>
362
  ''')
363
 
364
+ @app.route('/static/sound.mp3')
365
+ def serve_sound():
366
+ if os.path.exists('sound.mp3'):
367
+ with open('sound.mp3', 'rb') as f:
368
+ return Response(f.read(), mimetype='audio/mpeg')
369
+ return 'Sound not found', 404
370
+
371
  if __name__ == '__main__':
372
+ port = int(os.environ.get('PORT', 7860))
 
 
 
 
 
 
 
 
 
 
 
 
373
  app.run(host='0.0.0.0', port=port, debug=False, threaded=True)