MySafeCode commited on
Commit
33a92eb
·
verified ·
1 Parent(s): 20c53db

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +481 -346
app.py CHANGED
@@ -1,13 +1,25 @@
1
  import pygame
2
  import numpy as np
3
- from flask import Flask, Response, render_template_string, request
4
  import time
5
  import os
 
 
 
 
 
6
 
7
  # Initialize Pygame headlessly
8
  os.environ['SDL_VIDEODRIVER'] = 'dummy'
9
  pygame.init()
10
- #pygame.mixer.init(frequency=44100, size=-16, channels=2)
 
 
 
 
 
 
 
11
 
12
  app = Flask(__name__)
13
 
@@ -19,9 +31,12 @@ class ShaderRenderer:
19
  self.mouse_y = height // 2
20
  self.start_time = time.time()
21
  self.surface = pygame.Surface((width, height))
 
 
 
22
 
23
  # Sound sources
24
- self.sound_source = 'none' # 'none', 'pygame', 'browser'
25
  self.pygame_sound = None
26
  self.pygame_playing = False
27
  self.sound_amp = 0.0
@@ -55,13 +70,20 @@ class ShaderRenderer:
55
  self.sound_amp = 0.0
56
 
57
  def render_frame(self):
58
- """Render a simple pattern that shader will transform"""
59
  t = time.time() - self.start_time
60
 
 
 
 
 
 
 
 
61
  # Clear
62
  self.surface.fill((20, 20, 30))
63
 
64
- # Draw TOP marker (should be at top of canvas)
65
  pygame.draw.rect(self.surface, (255, 100, 100), (10, 10, 100, 30))
66
  font = pygame.font.Font(None, 24)
67
  text = font.render("TOP", True, (255, 255, 255))
@@ -102,63 +124,197 @@ class ShaderRenderer:
102
  pygame.draw.rect(self.surface, (100, 255, 100),
103
  (self.width-220, 10, meter_width, 20))
104
 
105
- # Shader indicator
106
- shader_text = font.render("SHADER ON", True, (255, 255, 0))
107
- self.surface.blit(shader_text, (self.width-150, self.height-30))
108
 
109
  return pygame.image.tostring(self.surface, 'RGB')
110
 
111
  def get_frame(self):
112
  return self.render_frame()
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  renderer = ShaderRenderer()
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  @app.route('/')
117
  def index():
118
  return render_template_string('''
119
  <!DOCTYPE html>
120
  <html>
121
  <head>
122
- <title>Pygame + WebGL Shader + Sound</title>
123
  <style>
124
  body {
125
  margin: 0;
126
  background: #0a0a0a;
127
  color: white;
128
- font-family: Arial, sans-serif;
129
  display: flex;
130
- flex-direction: column;
131
  justify-content: center;
132
  align-items: center;
133
  min-height: 100vh;
134
  }
135
 
136
  .container {
 
 
137
  text-align: center;
138
  }
139
 
140
- canvas {
141
- width: 640px;
142
- height: 480px;
143
- border: 3px solid #4CAF50;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  border-radius: 8px;
145
- cursor: crosshair;
146
  display: block;
147
- margin: 20px 0;
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
 
150
  .controls {
151
  background: #1a1a1a;
 
152
  padding: 20px;
153
- border-radius: 8px;
154
  margin-top: 20px;
155
  }
156
 
157
- .button-group {
158
  display: flex;
159
  gap: 10px;
160
  justify-content: center;
161
- margin: 15px 0;
162
  flex-wrap: wrap;
163
  }
164
 
@@ -168,427 +324,402 @@ def index():
168
  border: none;
169
  padding: 12px 24px;
170
  font-size: 16px;
171
- border-radius: 5px;
172
  cursor: pointer;
173
  transition: all 0.3s;
174
- min-width: 120px;
 
 
175
  }
176
 
177
  button:hover {
178
- transform: scale(1.05);
 
179
  }
180
 
181
  button.active {
182
  background: #4CAF50;
 
183
  box-shadow: 0 0 20px #4CAF50;
184
  }
185
 
186
- button.shader.active {
187
- background: #ffaa00;
188
- color: black;
189
- }
190
-
191
- button.sound.active {
192
- background: #4CAF50;
193
- }
194
-
195
- button.browser.active {
196
- background: #2196F3;
197
  }
198
 
199
- .status-bar {
200
  display: flex;
201
- justify-content: space-between;
202
  align-items: center;
203
- margin-top: 15px;
204
- padding: 10px;
205
- background: #222;
206
- border-radius: 5px;
207
  }
208
 
209
- .indicator {
210
- padding: 5px 10px;
211
- border-radius: 4px;
212
  font-size: 14px;
213
  }
214
 
215
- .indicator.none { background: #444; }
216
- .indicator.pygame { background: #4CAF50; }
217
- .indicator.browser { background: #2196F3; }
218
- .indicator.shader-on { background: #ffaa00; color: black; }
219
- .indicator.shader-off { background: #666; }
220
-
221
- .meter {
222
- width: 200px;
223
- height: 20px;
224
  background: #333;
225
- border-radius: 10px;
226
- overflow: hidden;
 
 
227
  }
228
 
229
- .meter-fill {
230
- height: 100%;
231
- width: 0%;
232
- background: linear-gradient(90deg, #4CAF50, #2196F3);
233
- transition: width 0.05s;
 
234
  }
235
 
236
  .badge {
237
  display: inline-block;
238
- padding: 5px 10px;
239
  border-radius: 4px;
240
- margin-left: 10px;
241
- font-size: 12px;
242
  }
243
 
244
- .badge.on {
245
  background: #4CAF50;
246
- color: white;
247
  }
248
 
249
- .badge.off {
250
- background: #ff4444;
251
- color: white;
252
  }
253
 
254
- .color-sample {
255
- display: inline-block;
256
- width: 12px;
257
- height: 12px;
258
- border-radius: 3px;
259
- margin: 0 5px;
260
  }
261
 
262
  .info-text {
263
- font-size: 12px;
264
  color: #666;
265
- margin-top: 15px;
 
266
  }
267
  </style>
268
  </head>
269
  <body>
270
  <div class="container">
271
- <h1>🎮 Pygame + WebGL Shader + Sound</h1>
272
 
273
- <canvas id="canvas" width="640" height="480"></canvas>
 
 
 
274
 
275
  <div class="controls">
276
- <div class="button-group">
277
- <button id="shaderBtn" class="shader active" onclick="toggleShader()">
278
- 🔮 SHADER ON
279
- </button>
280
- <span id="shaderBadge" class="badge on">EFFECTS ACTIVE</span>
281
- </div>
282
-
283
- <h3>🔊 Sound Source</h3>
284
- <div class="button-group">
285
- <button id="btnNone" class="sound active" onclick="setSoundSource('none')">🔇 None</button>
286
- <button id="btnPygame" class="sound" onclick="setSoundSource('pygame')">🎮 Pygame</button>
287
- <button id="btnBrowser" class="sound" onclick="setSoundSource('browser')">🌐 Browser</button>
288
  </div>
289
 
290
- <div class="status-bar">
291
- <div>
292
- <span id="sourceIndicator" class="indicator none">Source: None</span>
293
- <span id="shaderIndicator" class="indicator shader-on" style="margin-left: 10px;">Shader: ON</span>
 
 
 
 
294
  </div>
295
- <div class="meter">
296
- <div id="soundMeter" class="meter-fill"></div>
 
297
  </div>
298
  </div>
299
 
 
 
 
 
300
  <div class="info-text">
301
- <span class="color-sample" style="background: #ff6464;"></span> No sound
302
- <span class="color-sample" style="background: #64ff64;"></span> Pygame sound
303
- <span class="color-sample" style="background: #6464ff;"></span> Browser sound
304
- | <span style="color: #ff6464;">🔴 TOP</span> marker should be at top
305
- | <span style="color: #64ff64;">🟢 BOTTOM</span> at bottom
306
  </div>
307
  </div>
308
  </div>
309
 
310
- <!-- Audio element for browser sound -->
311
- <audio id="browserAudio" loop style="display:none;">
312
- <source src="/static/sound.mp3" type="audio/mpeg">
313
- </audio>
314
-
315
- <!-- WebGL Shader -->
316
- <script id="vertex-shader" type="x-shader/x-vertex">
317
- attribute vec2 position;
318
- varying vec2 vUv;
319
- void main() {
320
- // Flip Y coordinate to fix Pygame orientation
321
- vUv = vec2(position.x * 0.5 + 0.5, 1.0 - (position.y * 0.5 + 0.5));
322
- gl_Position = vec4(position, 0.0, 1.0);
323
- }
324
- </script>
325
-
326
- <script id="fragment-shader" type="x-shader/x-fragment">
327
- precision highp float;
328
-
329
- uniform sampler2D uTexture;
330
- uniform float uTime;
331
- uniform vec2 uMouse;
332
- uniform vec2 uResolution;
333
- uniform bool uShaderEnabled;
334
-
335
- varying vec2 vUv;
336
-
337
- void main() {
338
- // Get pixel from pygame texture (now correctly oriented)
339
- vec4 color = texture2D(uTexture, vUv);
340
-
341
- if (uShaderEnabled) {
342
- // SHADER EFFECTS ON
343
-
344
- // 1. Time-based color shift
345
- color.r += sin(uTime + vUv.x * 10.0) * 0.2;
346
- color.g += cos(uTime + vUv.y * 10.0) * 0.2;
347
-
348
- // 2. Mouse ripple effect
349
- float dist = distance(vUv, uMouse);
350
- if (dist < 0.2) {
351
- float ripple = sin(dist * 50.0 - uTime * 5.0) * 0.5 + 0.5;
352
- color.rgb += vec3(0.5, 0.2, 0.8) * ripple * 0.5;
353
- }
354
-
355
- // 3. Scanlines
356
- float scanline = sin(vUv.y * uResolution.y * 2.0 + uTime * 10.0) * 0.1 + 0.9;
357
- color.rgb *= scanline;
358
-
359
- // 4. Edge glow
360
- float edge = 1.0 - abs(vUv.x - 0.5) * 2.0;
361
- edge *= 1.0 - abs(vUv.y - 0.5) * 2.0;
362
- color.rgb += vec3(0.2, 0.1, 0.5) * edge * sin(uTime) * 0.3;
363
- }
364
- // else: SHADER EFFECTS OFF - pure Pygame pixels
365
-
366
- gl_FragColor = color;
367
- }
368
- </script>
369
-
370
  <script>
371
- const canvas = document.getElementById('canvas');
372
- const gl = canvas.getContext('webgl');
373
- const browserAudio = document.getElementById('browserAudio');
374
-
375
- if (!gl) {
376
- alert('WebGL not supported!');
377
- }
378
-
379
- let texture = gl.createTexture();
380
- let startTime = Date.now() / 1000;
381
- let shaderEnabled = true;
382
- let mouse = [0.5, 0.5];
383
- let currentSource = 'none';
384
-
385
- // UI Elements
386
- const shaderBtn = document.getElementById('shaderBtn');
387
- const shaderBadge = document.getElementById('shaderBadge');
388
- const shaderIndicator = document.getElementById('shaderIndicator');
389
-
390
- function toggleShader() {
391
- shaderEnabled = !shaderEnabled;
392
 
393
- // Update UI
394
- if (shaderEnabled) {
395
- shaderBtn.className = 'shader active';
396
- shaderBtn.innerHTML = '🔮 SHADER ON';
397
- shaderBadge.className = 'badge on';
398
- shaderBadge.innerHTML = 'EFFECTS ACTIVE';
399
- shaderIndicator.className = 'indicator shader-on';
400
- shaderIndicator.innerHTML = 'Shader: ON';
401
- } else {
402
- shaderBtn.className = '';
403
- shaderBtn.innerHTML = '🎮 SHADER OFF';
404
- shaderBadge.className = 'badge off';
405
- shaderBadge.innerHTML = 'PURE PYGAME';
406
- shaderIndicator.className = 'indicator shader-off';
407
- shaderIndicator.innerHTML = 'Shader: OFF';
408
- }
409
 
410
- // Update shader uniform
411
- if (program) {
412
- gl.useProgram(program);
413
- const uShaderEnabled = gl.getUniformLocation(program, 'uShaderEnabled');
414
- gl.uniform1i(uShaderEnabled, shaderEnabled);
415
  }
416
- }
417
-
418
- // Sound meter animation
419
- let soundAmp = 0;
420
- function updateSoundMeter() {
421
- if (currentSource === 'browser' && !browserAudio.paused) {
422
- // Simulate amplitude from browser audio
423
- soundAmp = 0.3 + 0.2 * Math.sin(Date.now() * 0.01);
424
- document.getElementById('soundMeter').style.width = (soundAmp * 100) + '%';
425
- } else if (currentSource === 'pygame') {
426
- // Get amplitude from server
427
- fetch('/sound/amp')
428
- .then(res => res.json())
429
- .then(data => {
430
- soundAmp = data.amp;
431
- document.getElementById('soundMeter').style.width = (soundAmp * 100) + '%';
432
- });
433
- } else {
434
- soundAmp = 0;
435
- document.getElementById('soundMeter').style.width = '0%';
436
  }
437
- requestAnimationFrame(updateSoundMeter);
 
 
 
 
 
438
  }
439
- updateSoundMeter();
440
 
441
- function setSoundSource(source) {
442
- currentSource = source;
443
 
444
- // Update UI
445
- document.getElementById('btnNone').className = source === 'none' ? 'sound active' : 'sound';
446
- document.getElementById('btnPygame').className = source === 'pygame' ? 'sound active' : 'sound';
447
- document.getElementById('btnBrowser').className = source === 'browser' ? 'sound active' : 'sound';
448
 
449
- const indicator = document.getElementById('sourceIndicator');
450
- indicator.className = `indicator ${source}`;
451
- indicator.innerHTML = `Source: ${source.charAt(0).toUpperCase() + source.slice(1)}`;
452
 
453
- // Handle audio
454
- if (source === 'browser') {
455
- browserAudio.play().catch(e => console.log('Audio play failed:', e));
 
 
 
 
 
456
  } else {
457
- browserAudio.pause();
458
- browserAudio.currentTime = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  }
460
-
461
- // Tell server about source change
462
- fetch('/sound/source', {
463
- method: 'POST',
464
- headers: {'Content-Type': 'application/json'},
465
- body: JSON.stringify({source: source})
466
- });
467
  }
468
 
469
- // Mouse tracking
470
- canvas.addEventListener('mousemove', (e) => {
471
- const rect = canvas.getBoundingClientRect();
472
- mouse[0] = (e.clientX - rect.left) / rect.width;
473
- mouse[1] = 1.0 - (e.clientY - rect.top) / rect.height;
474
-
475
- const x = Math.round((e.clientX - rect.left) * 640 / rect.width);
476
- const y = Math.round((e.clientY - rect.top) * 480 / rect.height);
477
-
478
- fetch('/mouse', {
479
- method: 'POST',
480
- headers: {'Content-Type': 'application/json'},
481
- body: JSON.stringify({x, y})
482
- });
483
- });
484
-
485
- // Setup WebGL
486
- function createShader(type, source) {
487
- const shader = gl.createShader(type);
488
- gl.shaderSource(shader, source);
489
- gl.compileShader(shader);
490
- return shader;
 
491
  }
492
 
493
- // Compile shaders
494
- const vertexShader = createShader(gl.VERTEX_SHADER,
495
- document.getElementById('vertex-shader').textContent);
496
- const fragmentShader = createShader(gl.FRAGMENT_SHADER,
497
- document.getElementById('fragment-shader').textContent);
498
 
499
- // Create program
500
- const program = gl.createProgram();
501
- gl.attachShader(program, vertexShader);
502
- gl.attachShader(program, fragmentShader);
503
- gl.linkProgram(program);
504
- gl.useProgram(program);
505
-
506
- // Set up fullscreen quad
507
- const vertices = new Float32Array([
508
- -1, -1, 1, -1, -1, 1,
509
- -1, 1, 1, -1, 1, 1
510
- ]);
511
-
512
- const buffer = gl.createBuffer();
513
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
514
- gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
515
-
516
- const position = gl.getAttribLocation(program, 'position');
517
- gl.enableVertexAttribArray(position);
518
- gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
519
-
520
- // Get uniform locations
521
- const uTexture = gl.getUniformLocation(program, 'uTexture');
522
- const uTime = gl.getUniformLocation(program, 'uTime');
523
- const uMouse = gl.getUniformLocation(program, 'uMouse');
524
- const uResolution = gl.getUniformLocation(program, 'uResolution');
525
- const uShaderEnabled = gl.getUniformLocation(program, 'uShaderEnabled');
526
-
527
- gl.uniform1i(uTexture, 0);
528
- gl.uniform2f(uResolution, canvas.width, canvas.height);
529
- gl.uniform1i(uShaderEnabled, shaderEnabled);
530
-
531
- // Texture setup
532
- gl.bindTexture(gl.TEXTURE_2D, texture);
533
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
534
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
535
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
536
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
537
-
538
- // Main loop
539
- function update() {
540
- fetch('/frame')
541
- .then(res => res.arrayBuffer())
542
- .then(buffer => {
543
- // Update texture with new frame
544
- gl.bindTexture(gl.TEXTURE_2D, texture);
545
- gl.texImage2D(
546
- gl.TEXTURE_2D, 0, gl.RGB, 640, 480, 0,
547
- gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array(buffer)
548
- );
549
-
550
- // Update uniforms
551
- const time = (Date.now() / 1000) - startTime;
552
- gl.uniform1f(uTime, time);
553
- gl.uniform2f(uMouse, mouse[0], mouse[1]);
554
-
555
- // Draw
556
- gl.drawArrays(gl.TRIANGLES, 0, 6);
557
-
558
- requestAnimationFrame(update);
559
- });
560
- }
561
-
562
- update();
563
  </script>
564
  </body>
565
  </html>
566
  ''')
567
 
568
- @app.route('/frame')
569
- def frame():
570
- """Return raw RGB bytes"""
571
- return Response(renderer.get_frame(), mimetype='application/octet-stream')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
 
573
  @app.route('/mouse', methods=['POST'])
574
  def mouse():
 
575
  data = request.json
576
  renderer.set_mouse(data['x'], data['y'])
577
  return 'OK'
578
 
579
  @app.route('/sound/source', methods=['POST'])
580
  def sound_source():
 
581
  data = request.json
582
  renderer.set_sound_source(data['source'])
583
  return 'OK'
584
 
585
  @app.route('/sound/amp')
586
  def sound_amp():
 
587
  return {'amp': renderer.sound_amp}
588
 
589
- # Serve static sound file
590
  @app.route('/static/sound.mp3')
591
  def serve_sound():
 
592
  if os.path.exists('sound.mp3'):
593
  with open('sound.mp3', 'rb') as f:
594
  return Response(f.read(), mimetype='audio/mpeg')
@@ -596,10 +727,14 @@ def serve_sound():
596
 
597
  if __name__ == '__main__':
598
  print("\n" + "="*70)
599
- print("🎮 Pygame + WebGL Shader + Sound")
600
  print("="*70)
601
- print("🌐 Starting on Hugging Face Spaces")
602
- print("\n🔮 Shader Toggle: See pure Pygame vs. effects")
 
 
 
603
  print("="*70 + "\n")
604
- port = int(os.environ.get('PORT', 7860)) # Use HF port
605
- app.run(host='0.0.0.0', port=port, debug=False)
 
 
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
  # Initialize Pygame headlessly
13
  os.environ['SDL_VIDEODRIVER'] = 'dummy'
14
  pygame.init()
15
+
16
+ # Try to initialize mixer, but don't crash if it fails
17
+ try:
18
+ pygame.mixer.init(frequency=44100, size=-16, channels=2)
19
+ print("✅ Audio mixer initialized")
20
+ except Exception as e:
21
+ print(f"⚠️ Audio mixer not available: {e}")
22
+ print(" Continuing without sound...")
23
 
24
  app = Flask(__name__)
25
 
 
31
  self.mouse_y = height // 2
32
  self.start_time = time.time()
33
  self.surface = pygame.Surface((width, height))
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'
40
  self.pygame_sound = None
41
  self.pygame_playing = False
42
  self.sound_amp = 0.0
 
70
  self.sound_amp = 0.0
71
 
72
  def render_frame(self):
73
+ """Render the pygame frame"""
74
  t = time.time() - self.start_time
75
 
76
+ # Calculate FPS
77
+ self.frame_count += 1
78
+ if time.time() - self.last_frame_time > 1.0:
79
+ self.fps = self.frame_count
80
+ self.frame_count = 0
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))
 
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
140
+ img = np.frombuffer(frame, dtype=np.uint8).reshape((self.height, self.width, 3))
141
+ # Convert RGB to BGR for OpenCV
142
+ img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
143
+ # Encode as JPEG
144
+ _, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, quality])
145
+ return jpeg.tobytes()
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;
259
+ font-family: 'Segoe UI', Arial, sans-serif;
260
  display: flex;
 
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
 
 
324
  border: none;
325
  padding: 12px 24px;
326
  font-size: 16px;
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')
 
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)