MySafeCode commited on
Commit
3332c15
·
verified ·
1 Parent(s): 33a92eb

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +417 -449
app.py CHANGED
@@ -9,6 +9,33 @@ import threading
9
  import queue
10
  import uuid
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  # Initialize Pygame headlessly
13
  os.environ['SDL_VIDEODRIVER'] = 'dummy'
14
  pygame.init()
@@ -24,7 +51,7 @@ except Exception as e:
24
  app = Flask(__name__)
25
 
26
  class ShaderRenderer:
27
- def __init__(self, width=640, height=480):
28
  self.width = width
29
  self.height = height
30
  self.mouse_x = width // 2
@@ -34,6 +61,7 @@ class ShaderRenderer:
34
  self.frame_count = 0
35
  self.last_frame_time = time.time()
36
  self.fps = 0
 
37
 
38
  # Sound sources
39
  self.sound_source = 'none'
@@ -69,6 +97,16 @@ class ShaderRenderer:
69
  self.pygame_playing = False
70
  self.sound_amp = 0.0
71
 
 
 
 
 
 
 
 
 
 
 
72
  def render_frame(self):
73
  """Render the pygame frame"""
74
  t = time.time() - self.start_time
@@ -81,59 +119,104 @@ class ShaderRenderer:
81
  self.last_frame_time = time.time()
82
 
83
  # Clear
84
- self.surface.fill((20, 20, 30))
 
 
 
 
85
 
86
  # Draw TOP marker
87
- pygame.draw.rect(self.surface, (255, 100, 100), (10, 10, 100, 30))
88
- font = pygame.font.Font(None, 24)
89
  text = font.render("TOP", True, (255, 255, 255))
90
- self.surface.blit(text, (20, 15))
91
 
92
  # Draw BOTTOM marker
93
- pygame.draw.rect(self.surface, (100, 255, 100), (10, self.height-40, 100, 30))
94
  text = font.render("BOTTOM", True, (0, 0, 0))
95
- self.surface.blit(text, (20, self.height-35))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- # Draw moving circle
98
- circle_size = 30 + int(20 * np.sin(t * 2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  # Color code based on sound source
101
  if self.sound_source == 'pygame':
102
- color = (100, 255, 100) # Green for pygame sound
103
  elif self.sound_source == 'browser':
104
- color = (100, 100, 255) # Blue for browser sound
105
  else:
106
- color = (255, 100, 100) # Red for no sound
107
 
108
  pygame.draw.circle(self.surface, color,
109
  (self.mouse_x, self.mouse_y), circle_size)
110
 
111
- # Draw grid
112
- for x in range(0, self.width, 50):
113
  alpha = int(40 + 20 * np.sin(x * 0.1 + t))
114
  pygame.draw.line(self.surface, (alpha, alpha, 50),
115
  (x, 0), (x, self.height))
116
- for y in range(0, self.height, 50):
117
  alpha = int(40 + 20 * np.cos(y * 0.1 + t))
118
  pygame.draw.line(self.surface, (alpha, alpha, 50),
119
  (0, y), (self.width, y))
120
 
121
- # Sound meter
122
- meter_width = int(200 * self.sound_amp)
123
- pygame.draw.rect(self.surface, (60, 60, 60), (self.width-220, 10, 200, 20))
124
  pygame.draw.rect(self.surface, (100, 255, 100),
125
- (self.width-220, 10, meter_width, 20))
126
 
127
- # FPS counter
128
- fps_text = font.render(f"FPS: {self.fps}", True, (255, 255, 0))
129
- self.surface.blit(fps_text, (self.width-150, self.height-60))
130
 
131
  return pygame.image.tostring(self.surface, 'RGB')
132
 
133
  def get_frame(self):
134
  return self.render_frame()
135
 
136
- def get_frame_jpeg(self, quality=80):
137
  """Return frame as JPEG"""
138
  frame = self.get_frame()
139
  # Convert to numpy array for OpenCV
@@ -146,113 +229,164 @@ class ShaderRenderer:
146
 
147
  renderer = ShaderRenderer()
148
 
149
- # Streaming managers
150
- class StreamManager:
151
- def __init__(self):
152
- self.streams = {}
153
-
154
- def create_mjpeg_stream(self):
155
- stream_id = str(uuid.uuid4())
156
- self.streams[stream_id] = {
157
- 'type': 'mjpeg',
158
- 'active': True,
159
- 'clients': 0
160
- }
161
- return stream_id
162
 
163
- def create_ffmpeg_stream(self, format_type='webm'):
164
- stream_id = str(uuid.uuid4())
165
-
166
- # FFmpeg command based on format
167
- if format_type == 'webm':
168
- cmd = [
169
- 'ffmpeg',
170
- '-f', 'rawvideo',
171
- '-pix_fmt', 'rgb24',
172
- '-s', '640x480',
173
- '-r', '30',
174
- '-i', '-',
175
- '-c:v', 'libvpx-vp9',
176
- '-b:v', '1M',
177
- '-cpu-used', '4',
178
- '-deadline', 'realtime',
179
- '-f', 'webm',
180
- '-'
181
- ]
182
- mimetype = 'video/webm'
183
- else: # mp4
184
- cmd = [
185
- 'ffmpeg',
186
- '-f', 'rawvideo',
187
- '-pix_fmt', 'rgb24',
188
- '-s', '640x480',
189
- '-r', '30',
190
- '-i', '-',
191
- '-c:v', 'libx264',
192
- '-preset', 'ultrafast',
193
- '-tune', 'zerolatency',
194
- '-b:v', '1M',
195
- '-f', 'mp4',
196
- '-movflags', 'frag_keyframe+empty_moov',
197
- '-'
198
- ]
199
- mimetype = 'video/mp4'
200
-
201
- # Start FFmpeg process
202
- process = subprocess.Popen(
203
- cmd,
204
- stdin=subprocess.PIPE,
205
- stdout=subprocess.PIPE,
206
- stderr=subprocess.DEVNULL,
207
- bufsize=0
208
- )
209
-
210
- frame_queue = queue.Queue(maxsize=30)
211
 
212
- self.streams[stream_id] = {
213
- 'type': format_type,
214
- 'mimetype': mimetype,
215
- 'active': True,
216
- 'process': process,
217
- 'queue': frame_queue,
218
- 'clients': 0
219
- }
220
 
221
- # Start frame pusher thread
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  def push_frames():
223
- while self.streams.get(stream_id, {}).get('active', False):
224
  try:
225
  frame = renderer.get_frame()
226
  process.stdin.write(frame)
227
  except:
228
  break
229
- process.terminate()
230
 
231
  threading.Thread(target=push_frames, daemon=True).start()
232
 
233
- return stream_id
234
-
235
- def get_stream(self, stream_id):
236
- return self.streams.get(stream_id)
 
237
 
238
- def close_stream(self, stream_id):
239
- if stream_id in self.streams:
240
- stream = self.streams[stream_id]
241
- if 'process' in stream:
242
- stream['process'].terminate()
243
- del self.streams[stream_id]
 
244
 
245
- stream_manager = StreamManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  @app.route('/')
248
  def index():
249
- return render_template_string('''
250
  <!DOCTYPE html>
251
  <html>
252
  <head>
253
- <title>🎮 Pygame + Multi-format Streaming</title>
254
  <style>
255
- body {
256
  margin: 0;
257
  background: #0a0a0a;
258
  color: white;
@@ -261,64 +395,72 @@ def index():
261
  justify-content: center;
262
  align-items: center;
263
  min-height: 100vh;
264
- }
265
 
266
- .container {
267
- max-width: 900px;
268
  padding: 20px;
269
  text-align: center;
270
- }
271
 
272
- h1 {
273
  color: #4CAF50;
274
  margin-bottom: 20px;
275
- text-shadow: 0 0 10px rgba(76, 175, 80, 0.3);
276
- }
277
 
278
- .video-container {
279
  background: #000;
280
  border-radius: 12px;
281
  padding: 5px;
282
  margin: 20px 0;
283
  box-shadow: 0 0 30px rgba(76, 175, 80, 0.2);
284
- }
 
285
 
286
- #videoPlayer {
287
  width: 100%;
288
- max-width: 640px;
289
  height: auto;
290
  border-radius: 8px;
291
  display: block;
292
  margin: 0 auto;
293
  background: #111;
294
- }
 
295
 
296
- #mjpegImg {
297
- width: 100%;
298
- max-width: 640px;
299
- height: auto;
300
- border-radius: 8px;
301
  display: none;
302
- margin: 0 auto;
303
- background: #111;
304
- }
305
 
306
- .controls {
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  background: #1a1a1a;
308
  border-radius: 12px;
309
  padding: 20px;
310
  margin-top: 20px;
311
- }
312
 
313
- .format-buttons {
314
  display: flex;
315
  gap: 10px;
316
  justify-content: center;
317
  margin: 20px 0;
318
  flex-wrap: wrap;
319
- }
320
 
321
- button {
322
  background: #333;
323
  color: white;
324
  border: none;
@@ -327,414 +469,240 @@ def index():
327
  border-radius: 8px;
328
  cursor: pointer;
329
  transition: all 0.3s;
330
- min-width: 100px;
331
  font-weight: bold;
332
  border: 1px solid #444;
333
- }
334
 
335
- button:hover {
336
  transform: translateY(-2px);
337
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
338
- }
339
 
340
- button.active {
341
  background: #4CAF50;
342
  border-color: #4CAF50;
343
  box-shadow: 0 0 20px #4CAF50;
344
- }
345
 
346
- .status-panel {
347
  background: #222;
348
  border-radius: 8px;
349
  padding: 15px;
350
  margin-top: 20px;
351
  display: flex;
352
  justify-content: space-around;
353
- align-items: center;
354
  flex-wrap: wrap;
355
  gap: 15px;
356
- }
357
 
358
- .status-item {
359
  display: flex;
360
  align-items: center;
361
  gap: 10px;
362
- }
363
 
364
- .status-label {
365
  color: #888;
366
  font-size: 14px;
367
- }
368
 
369
- .status-value {
370
  background: #333;
371
  padding: 5px 12px;
372
  border-radius: 20px;
373
  font-size: 14px;
374
  font-weight: bold;
375
- }
376
-
377
- .browser-support {
378
- font-size: 12px;
379
- color: #666;
380
- margin-top: 15px;
381
- padding: 10px;
382
- border-top: 1px solid #333;
383
- }
384
-
385
- .badge {
386
- display: inline-block;
387
- padding: 3px 8px;
388
- border-radius: 4px;
389
- font-size: 11px;
390
- margin-left: 5px;
391
- }
392
 
393
- .badge.green {
394
- background: #4CAF50;
395
- }
396
-
397
- .badge.yellow {
398
- background: #ffaa00;
399
- color: black;
400
- }
401
-
402
- .badge.red {
403
- background: #ff4444;
404
- }
405
 
406
- .info-text {
407
- color: #666;
408
- font-size: 12px;
409
- margin-top: 10px;
410
- }
 
411
  </style>
412
  </head>
413
  <body>
414
  <div class="container">
415
- <h1>🎮 Pygame + Multi-format Streaming</h1>
416
 
417
  <div class="video-container">
418
  <video id="videoPlayer" autoplay controls muted></video>
419
- <img id="mjpegImg" src="">
 
420
  </div>
421
 
422
  <div class="controls">
423
- <h3>📡 Streaming Format</h3>
424
  <div class="format-buttons">
425
  <button id="btnMjpeg" onclick="setFormat('mjpeg')" class="active">📸 MJPEG</button>
426
- <button id="btnWebm" onclick="setFormat('webm')">🎥 WebM (VP9)</button>
427
- <button id="btnMp4" onclick="setFormat('mp4')">🎬 MP4 (H.264)</button>
 
 
 
 
 
 
 
 
 
 
428
  </div>
429
 
430
  <div class="status-panel">
431
  <div class="status-item">
432
- <span class="status-label">Current Format:</span>
433
  <span id="currentFormat" class="status-value">MJPEG</span>
434
  </div>
435
  <div class="status-item">
436
- <span class="status-label">Browser Support:</span>
437
- <span id="browserSupport" class="status-value">Checking...</span>
438
  </div>
439
  <div class="status-item">
440
- <span class="status-label">Stream Status:</span>
441
- <span id="streamStatus" class="status-value">🟢 Active</span>
442
  </div>
443
  </div>
444
-
445
- <div class="browser-support" id="supportDetails">
446
- Testing browser capabilities...
447
- </div>
448
-
449
- <div class="info-text">
450
- ⚡ MJPEG: Lowest CPU, universal support<br>
451
- 🎥 WebM: Efficient compression, best for Chrome/Firefox<br>
452
- 🎬 MP4: Universal support, hardware accelerated
453
- </div>
454
  </div>
455
  </div>
456
 
 
 
 
 
457
  <script>
458
  const videoPlayer = document.getElementById('videoPlayer');
459
  const mjpegImg = document.getElementById('mjpegImg');
 
 
460
  let currentFormat = 'mjpeg';
461
- let streamActive = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
 
463
- // Check browser capabilities
464
- function checkBrowserSupport() {
465
- const video = document.createElement('video');
466
- const support = {
467
- webm: video.canPlayType('video/webm; codecs="vp9, vorbis"'),
468
- webmVP8: video.canPlayType('video/webm; codecs="vp8, vorbis"'),
469
- mp4: video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"'),
470
- mp4H264: video.canPlayType('video/mp4; codecs="avc1.64001E"'),
471
- mjpeg: true
472
- };
473
 
474
- let supportText = '';
475
- let details = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
- if (support.webm) {
478
- supportText += ' WebM VP9 ';
479
- details += 'WebM VP9: ' + support.webm + '<br>';
480
- }
481
- if (support.mp4) {
482
- supportText += '✓ MP4 H.264 ';
483
- details += 'MP4 H.264: ' + support.mp4 + '<br>';
484
- }
485
- supportText += '✓ MJPEG';
486
 
487
- document.getElementById('browserSupport').innerHTML = supportText;
488
- document.getElementById('supportDetails').innerHTML = details;
 
 
 
 
489
 
490
- return support;
491
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
- function setFormat(format) {
 
494
  currentFormat = format;
495
 
496
- // Update button states
497
  document.getElementById('btnMjpeg').className = format === 'mjpeg' ? 'active' : '';
498
  document.getElementById('btnWebm').className = format === 'webm' ? 'active' : '';
499
  document.getElementById('btnMp4').className = format === 'mp4' ? 'active' : '';
500
-
501
  document.getElementById('currentFormat').innerHTML = format.toUpperCase();
502
 
503
- // Hide both players first
504
  videoPlayer.style.display = 'none';
505
  mjpegImg.style.display = 'none';
506
 
507
- if (format === 'mjpeg') {
508
- // Use img tag for MJPEG
509
  mjpegImg.style.display = 'block';
510
- mjpegImg.src = '/video/mjpeg?' + Date.now(); // Add timestamp to prevent caching
511
- } else {
512
- // Use video tag for WebM/MP4
513
  videoPlayer.style.display = 'block';
514
-
515
- // Stop current playback
516
- videoPlayer.pause();
517
- videoPlayer.removeAttribute('src');
518
- videoPlayer.load();
519
-
520
- // Set new source
521
- const source = document.createElement('source');
522
- if (format === 'webm') {
523
- source.src = '/video/webm?' + Date.now();
524
- source.type = 'video/webm';
525
- } else {
526
- source.src = '/video/mp4?' + Date.now();
527
- source.type = 'video/mp4';
528
- }
529
-
530
- videoPlayer.appendChild(source);
531
- videoPlayer.load();
532
  videoPlayer.play().catch(e => console.log('Playback error:', e));
533
- }
534
- }
535
-
536
- // Monitor stream health
537
- function checkStreamHealth() {
538
- if (currentFormat === 'mjpeg') {
539
- // For MJPEG, check if image is loading
540
- mjpegImg.onerror = function() {
541
- document.getElementById('streamStatus').innerHTML = '🔴 Error';
542
- streamActive = false;
543
- };
544
- mjpegImg.onload = function() {
545
- document.getElementById('streamStatus').innerHTML = '🟢 Active';
546
- streamActive = true;
547
- };
548
- } else {
549
- // For video, check if playing
550
- videoPlayer.onerror = function() {
551
- document.getElementById('streamStatus').innerHTML = '🔴 Error';
552
- streamActive = false;
553
- };
554
- videoPlayer.onplaying = function() {
555
- document.getElementById('streamStatus').innerHTML = '🟢 Active';
556
- streamActive = true;
557
- };
558
- }
559
- }
560
 
561
  // Initialize
562
- checkBrowserSupport();
563
- setFormat('mjpeg'); // Start with MJPEG for reliability
564
- checkStreamHealth();
565
-
566
- // Reconnect on error
567
- setInterval(() => {
568
- if (!streamActive) {
569
- console.log('Attempting to reconnect...');
570
- setFormat(currentFormat);
571
- }
572
- }, 5000);
573
  </script>
574
  </body>
575
  </html>
576
  ''')
577
 
578
- @app.route('/video/mjpeg')
579
- def video_mjpeg():
580
- """MJPEG streaming endpoint"""
581
- def generate():
582
- while True:
583
- frame = renderer.get_frame_jpeg(quality=70)
584
- yield (b'--frame\r\n'
585
- b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
586
- # Small delay to control frame rate
587
- time.sleep(1/30)
588
-
589
- return Response(
590
- generate(),
591
- mimetype='multipart/x-mixed-replace; boundary=frame'
592
- )
593
-
594
- @app.route('/video/webm')
595
- def video_webm():
596
- """WebM streaming endpoint"""
597
- cmd = [
598
- 'ffmpeg',
599
- '-f', 'rawvideo',
600
- '-pix_fmt', 'rgb24',
601
- '-s', '640x480',
602
- '-r', '30',
603
- '-i', '-',
604
- '-c:v', 'libvpx-vp9',
605
- '-b:v', '1M',
606
- '-cpu-used', '4',
607
- '-deadline', 'realtime',
608
- '-f', 'webm',
609
- '-'
610
- ]
611
-
612
- process = subprocess.Popen(
613
- cmd,
614
- stdin=subprocess.PIPE,
615
- stdout=subprocess.PIPE,
616
- stderr=subprocess.DEVNULL,
617
- bufsize=0
618
- )
619
-
620
- def generate():
621
- # Frame pusher thread
622
- def push_frames():
623
- while True:
624
- try:
625
- frame = renderer.get_frame()
626
- process.stdin.write(frame)
627
- except:
628
- break
629
-
630
- threading.Thread(target=push_frames, daemon=True).start()
631
-
632
- # Read and yield encoded data
633
- while True:
634
- data = process.stdout.read(4096)
635
- if not data:
636
- break
637
- yield data
638
-
639
- return Response(
640
- generate(),
641
- mimetype='video/webm',
642
- headers={
643
- 'Cache-Control': 'no-cache',
644
- 'Transfer-Encoding': 'chunked'
645
- }
646
- )
647
-
648
- @app.route('/video/mp4')
649
- def video_mp4():
650
- """MP4 streaming endpoint"""
651
- cmd = [
652
- 'ffmpeg',
653
- '-f', 'rawvideo',
654
- '-pix_fmt', 'rgb24',
655
- '-s', '640x480',
656
- '-r', '30',
657
- '-i', '-',
658
- '-c:v', 'libx264',
659
- '-preset', 'ultrafast',
660
- '-tune', 'zerolatency',
661
- '-b:v', '1M',
662
- '-f', 'mp4',
663
- '-movflags', 'frag_keyframe+empty_moov',
664
- '-'
665
- ]
666
-
667
- process = subprocess.Popen(
668
- cmd,
669
- stdin=subprocess.PIPE,
670
- stdout=subprocess.PIPE,
671
- stderr=subprocess.DEVNULL,
672
- bufsize=0
673
- )
674
-
675
- def generate():
676
- def push_frames():
677
- while True:
678
- try:
679
- frame = renderer.get_frame()
680
- process.stdin.write(frame)
681
- except:
682
- break
683
-
684
- threading.Thread(target=push_frames, daemon=True).start()
685
-
686
- while True:
687
- data = process.stdout.read(4096)
688
- if not data:
689
- break
690
- yield data
691
-
692
- return Response(
693
- generate(),
694
- mimetype='video/mp4',
695
- headers={
696
- 'Cache-Control': 'no-cache',
697
- 'Transfer-Encoding': 'chunked'
698
- }
699
- )
700
-
701
- @app.route('/mouse', methods=['POST'])
702
- def mouse():
703
- """Keep mouse endpoint for interactivity"""
704
- data = request.json
705
- renderer.set_mouse(data['x'], data['y'])
706
- return 'OK'
707
-
708
- @app.route('/sound/source', methods=['POST'])
709
- def sound_source():
710
- """Keep sound source endpoint"""
711
- data = request.json
712
- renderer.set_sound_source(data['source'])
713
- return 'OK'
714
-
715
- @app.route('/sound/amp')
716
- def sound_amp():
717
- """Keep sound amp endpoint for compatibility"""
718
- return {'amp': renderer.sound_amp}
719
-
720
- @app.route('/static/sound.mp3')
721
- def serve_sound():
722
- """Serve sound file"""
723
- if os.path.exists('sound.mp3'):
724
- with open('sound.mp3', 'rb') as f:
725
- return Response(f.read(), mimetype='audio/mpeg')
726
- return 'Sound not found', 404
727
-
728
  if __name__ == '__main__':
729
  print("\n" + "="*70)
730
- print("🎮 Pygame + Multi-format Streaming")
731
  print("="*70)
 
732
  print("📡 Streaming endpoints:")
733
- print(" • MJPEG: /video/mjpeg - Low CPU, universal")
734
- print(" • WebM: /video/webm - VP9 codec, efficient")
735
- print(" • MP4: /video/mp4 - H.264, hardware accelerated")
736
- print("\n🌐 Main page: /")
 
737
  print("="*70 + "\n")
738
 
739
- port = int(os.environ.get('PORT', 7860))
740
  app.run(host='0.0.0.0', port=port, debug=False, threaded=True)
 
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()
 
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
 
61
  self.frame_count = 0
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'
 
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
 
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
 
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
  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;
439
+ background: rgba(0,0,0,0.7);
440
+ color: #4CAF50;
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;
 
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
 
579
+ <audio id="browserAudio" loop style="display:none;">
580
+ <source src="/static/sound.mp3" type="audio/mpeg">
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)