import pygame import numpy as np from flask import Flask, Response, render_template_string, request, jsonify import time import os import cv2 import subprocess import threading import queue import uuid # Initialize Pygame headlessly os.environ['SDL_VIDEODRIVER'] = 'dummy' pygame.init() # Try to initialize mixer, but don't crash if it fails try: pygame.mixer.init(frequency=44100, size=-16, channels=2) print("✅ Audio mixer initialized") except Exception as e: print(f"⚠️ Audio mixer not available: {e}") print(" Continuing without sound...") app = Flask(__name__) class ShaderRenderer: def __init__(self, width=640, height=480): self.width = width self.height = height self.mouse_x = width // 2 self.mouse_y = height // 2 self.start_time = time.time() self.surface = pygame.Surface((width, height)) self.frame_count = 0 self.last_frame_time = time.time() self.fps = 0 # Sound sources self.sound_source = 'none' self.pygame_sound = None self.pygame_playing = False self.sound_amp = 0.0 # Load pygame sound if available if os.path.exists('sound.mp3'): try: self.pygame_sound = pygame.mixer.Sound('sound.mp3') print("✅ Pygame sound loaded") except: print("⚠️ Could not load sound.mp3") def set_mouse(self, x, y): self.mouse_x = max(0, min(self.width, x)) self.mouse_y = max(0, min(self.height, y)) def set_sound_source(self, source): """Change sound source: none, pygame, browser""" self.sound_source = source # Handle pygame sound if source == 'pygame': if self.pygame_sound and not self.pygame_playing: self.pygame_sound.play(loops=-1) self.pygame_playing = True self.sound_amp = 0.5 else: if self.pygame_playing: pygame.mixer.stop() self.pygame_playing = False self.sound_amp = 0.0 def render_frame(self): """Render the pygame frame""" t = time.time() - self.start_time # Calculate FPS self.frame_count += 1 if time.time() - self.last_frame_time > 1.0: self.fps = self.frame_count self.frame_count = 0 self.last_frame_time = time.time() # Clear self.surface.fill((20, 20, 30)) # Draw TOP marker pygame.draw.rect(self.surface, (255, 100, 100), (10, 10, 100, 30)) font = pygame.font.Font(None, 24) text = font.render("TOP", True, (255, 255, 255)) self.surface.blit(text, (20, 15)) # Draw BOTTOM marker pygame.draw.rect(self.surface, (100, 255, 100), (10, self.height-40, 100, 30)) text = font.render("BOTTOM", True, (0, 0, 0)) self.surface.blit(text, (20, self.height-35)) # Draw moving circle circle_size = 30 + int(20 * np.sin(t * 2)) # Color code based on sound source if self.sound_source == 'pygame': color = (100, 255, 100) # Green for pygame sound elif self.sound_source == 'browser': color = (100, 100, 255) # Blue for browser sound else: color = (255, 100, 100) # Red for no sound pygame.draw.circle(self.surface, color, (self.mouse_x, self.mouse_y), circle_size) # Draw grid for x in range(0, self.width, 50): alpha = int(40 + 20 * np.sin(x * 0.1 + t)) pygame.draw.line(self.surface, (alpha, alpha, 50), (x, 0), (x, self.height)) for y in range(0, self.height, 50): alpha = int(40 + 20 * np.cos(y * 0.1 + t)) pygame.draw.line(self.surface, (alpha, alpha, 50), (0, y), (self.width, y)) # Sound meter meter_width = int(200 * self.sound_amp) pygame.draw.rect(self.surface, (60, 60, 60), (self.width-220, 10, 200, 20)) pygame.draw.rect(self.surface, (100, 255, 100), (self.width-220, 10, meter_width, 20)) # FPS counter fps_text = font.render(f"FPS: {self.fps}", True, (255, 255, 0)) self.surface.blit(fps_text, (self.width-150, self.height-60)) return pygame.image.tostring(self.surface, 'RGB') def get_frame(self): return self.render_frame() def get_frame_jpeg(self, quality=80): """Return frame as JPEG""" frame = self.get_frame() # Convert to numpy array for OpenCV img = np.frombuffer(frame, dtype=np.uint8).reshape((self.height, self.width, 3)) # Convert RGB to BGR for OpenCV img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # Encode as JPEG _, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, quality]) return jpeg.tobytes() renderer = ShaderRenderer() # Streaming managers class StreamManager: def __init__(self): self.streams = {} def create_mjpeg_stream(self): stream_id = str(uuid.uuid4()) self.streams[stream_id] = { 'type': 'mjpeg', 'active': True, 'clients': 0 } return stream_id def create_ffmpeg_stream(self, format_type='webm'): stream_id = str(uuid.uuid4()) # FFmpeg command based on format if format_type == 'webm': cmd = [ 'ffmpeg', '-f', 'rawvideo', '-pix_fmt', 'rgb24', '-s', '640x480', '-r', '30', '-i', '-', '-c:v', 'libvpx-vp9', '-b:v', '1M', '-cpu-used', '4', '-deadline', 'realtime', '-f', 'webm', '-' ] mimetype = 'video/webm' else: # mp4 cmd = [ 'ffmpeg', '-f', 'rawvideo', '-pix_fmt', 'rgb24', '-s', '640x480', '-r', '30', '-i', '-', '-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency', '-b:v', '1M', '-f', 'mp4', '-movflags', 'frag_keyframe+empty_moov', '-' ] mimetype = 'video/mp4' # Start FFmpeg process process = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=0 ) frame_queue = queue.Queue(maxsize=30) self.streams[stream_id] = { 'type': format_type, 'mimetype': mimetype, 'active': True, 'process': process, 'queue': frame_queue, 'clients': 0 } # Start frame pusher thread def push_frames(): while self.streams.get(stream_id, {}).get('active', False): try: frame = renderer.get_frame() process.stdin.write(frame) except: break process.terminate() threading.Thread(target=push_frames, daemon=True).start() return stream_id def get_stream(self, stream_id): return self.streams.get(stream_id) def close_stream(self, stream_id): if stream_id in self.streams: stream = self.streams[stream_id] if 'process' in stream: stream['process'].terminate() del self.streams[stream_id] stream_manager = StreamManager() @app.route('/') def index(): return render_template_string('''