FlaskedPygame2 / app.py
MySafeCode's picture
Update app.py
ebbe75d verified
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
# ============ CONFIGURATION ============
VIDEO_WIDTH = 1280
VIDEO_HEIGHT = 720
VIDEO_FPS = 30
JPEG_QUALITY = 70
STREAM_PORT = 7860
# Colors (RGB)
COLOR_TOP = (255, 100, 100)
COLOR_BOTTOM = (100, 255, 100)
COLOR_BG = (20, 20, 30)
COLOR_GRID = (50, 50, 70)
COLOR_FPS = (255, 255, 0)
COLOR_CLOCK = (0, 255, 255)
# Button colors
COLOR_BUTTON_NORMAL = (80, 80, 80)
COLOR_BUTTON_HOVER = (100, 100, 200)
COLOR_BUTTON_CLICKED = (0, 200, 0)
COLOR_BUTTON_BORDER = (200, 200, 200)
# Sound source colors
COLOR_SOUND_NONE = (255, 100, 100)
COLOR_SOUND_PYGAME = (100, 255, 100)
COLOR_SOUND_BROWSER = (100, 100, 255)
# =======================================
# 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=VIDEO_WIDTH, height=VIDEO_HEIGHT):
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
self.button_clicked = False
# 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 handle_click(self, x, y):
"""Handle mouse clicks on Pygame surface"""
button_rect = pygame.Rect(self.width-250, 120, 220, 50)
if button_rect.collidepoint(x, y):
self.button_clicked = not self.button_clicked
print(f"🎯 Button {'clicked!' if self.button_clicked else 'unclicked!'}")
return True
return False
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(COLOR_BG)
# Use larger font for 720p
font = pygame.font.Font(None, 36)
small_font = pygame.font.Font(None, 24)
# Draw TOP marker
pygame.draw.rect(self.surface, COLOR_TOP, (10, 10, 150, 40))
text = font.render("TOP", True, (255, 255, 255))
self.surface.blit(text, (30, 15))
# Draw BOTTOM marker
pygame.draw.rect(self.surface, COLOR_BOTTOM, (10, self.height-50, 150, 40))
text = font.render("BOTTOM", True, (0, 0, 0))
self.surface.blit(text, (20, self.height-45))
# ===== CLOCK DISPLAY =====
current_time = time.time()
seconds = int(current_time) % 60
hundredths = int((current_time * 100) % 100)
# Clock background
pygame.draw.rect(self.surface, (40, 40, 50), (self.width-250, 70, 220, 50))
pygame.draw.rect(self.surface, (100, 100, 150), (self.width-250, 70, 220, 50), 2)
# Clock text
time_str = f"{seconds:02d}.{hundredths:02d}s"
clock_text = font.render(time_str, True, COLOR_CLOCK)
self.surface.blit(clock_text, (self.width-230, 80))
# ===== CLICKABLE BUTTON =====
button_rect = pygame.Rect(self.width-250, 140, 220, 50)
# Check if mouse is over button
mouse_over = button_rect.collidepoint(self.mouse_x, self.mouse_y)
# Button color based on state and hover
if self.button_clicked:
button_color = COLOR_BUTTON_CLICKED
elif mouse_over:
button_color = COLOR_BUTTON_HOVER
else:
button_color = COLOR_BUTTON_NORMAL
# Draw button
pygame.draw.rect(self.surface, button_color, button_rect)
pygame.draw.rect(self.surface, COLOR_BUTTON_BORDER, button_rect, 3)
# Button text
if self.button_clicked:
btn_text = "✅ CLICKED!"
else:
btn_text = "🔘 CLICK ME"
text_surf = font.render(btn_text, True, (255, 255, 255))
text_rect = text_surf.get_rect(center=button_rect.center)
self.surface.blit(text_surf, text_rect)
# ===== MOVING CIRCLE =====
circle_size = 40 + int(30 * np.sin(t * 2))
# Color code based on sound source
if self.sound_source == 'pygame':
color = COLOR_SOUND_PYGAME
elif self.sound_source == 'browser':
color = COLOR_SOUND_BROWSER
else:
color = COLOR_SOUND_NONE
pygame.draw.circle(self.surface, color,
(self.mouse_x, self.mouse_y), circle_size)
# ===== GRID =====
for x in range(0, self.width, 70):
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, 70):
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(250 * self.sound_amp)
pygame.draw.rect(self.surface, (60, 60, 60), (self.width-270, 210, 250, 25))
pygame.draw.rect(self.surface, (100, 255, 100),
(self.width-270, 210, meter_width, 25))
# ===== FPS COUNTER =====
fps_text = small_font.render(f"FPS: {self.fps}", True, COLOR_FPS)
self.surface.blit(fps_text, (self.width-200, self.height-80))
return pygame.image.tostring(self.surface, 'RGB')
def get_frame(self):
return self.render_frame()
def get_frame_jpeg(self, quality=JPEG_QUALITY):
"""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 ENDPOINTS ============
@app.route('/video/mjpeg')
def video_mjpeg():
"""MJPEG streaming endpoint"""
def generate():
while True:
frame = renderer.get_frame_jpeg()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
time.sleep(1/VIDEO_FPS)
return Response(
generate(),
mimetype='multipart/x-mixed-replace; boundary=frame'
)
@app.route('/video/webm')
def video_webm():
"""WebM streaming endpoint"""
cmd = [
'ffmpeg',
'-f', 'rawvideo',
'-pix_fmt', 'rgb24',
'-s', f'{VIDEO_WIDTH}x{VIDEO_HEIGHT}',
'-r', str(VIDEO_FPS),
'-i', '-',
'-c:v', 'libvpx-vp9',
'-b:v', '2M',
'-cpu-used', '4',
'-deadline', 'realtime',
'-f', 'webm',
'-'
]
process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=0
)
def generate():
def push_frames():
while True:
try:
frame = renderer.get_frame()
process.stdin.write(frame)
except:
break
threading.Thread(target=push_frames, daemon=True).start()
while True:
data = process.stdout.read(4096)
if not data:
break
yield data
return Response(
generate(),
mimetype='video/webm',
headers={'Cache-Control': 'no-cache', 'Transfer-Encoding': 'chunked'}
)
@app.route('/video/mp4')
def video_mp4():
"""GPU-accelerated MP4 streaming using NVENC"""
cmd = [
'ffmpeg',
'-hwaccel', 'cuda', # Use GPU for decoding
'-hwaccel_output_format', 'cuda', # Keep data on GPU
'-f', 'rawvideo',
'-pix_fmt', 'rgb24',
'-s', f'{VIDEO_WIDTH}x{VIDEO_HEIGHT}',
'-r', '30',
'-i', '-',
'-c:v', 'h264_nvenc', # NVIDIA's GPU encoder
'-preset', 'p4', # p1-p7, p4 is good balance
'-tune', 'll', # Low latency tuning
'-b:v', '2M',
'-bufsize', '4M',
'-f', 'mp4',
'-movflags', 'frag_keyframe+empty_moov',
'-'
]
process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=0
)
def generate():
def push_frames():
while True:
try:
frame = renderer.get_frame()
process.stdin.write(frame)
except:
break
threading.Thread(target=push_frames, daemon=True).start()
while True:
data = process.stdout.read(4096)
if not data:
break
yield data
return Response(
generate(),
mimetype='video/mp4',
headers={'Cache-Control': 'no-cache', 'Transfer-Encoding': 'chunked'}
)
# ============ INTERACTIVITY ENDPOINTS ============
@app.route('/mouse', methods=['POST'])
def mouse():
data = request.json
renderer.set_mouse(data['x'], data['y'])
return 'OK'
@app.route('/click', methods=['POST'])
def click():
data = request.json
renderer.handle_click(data['x'], data['y'])
return 'OK'
@app.route('/sound/source', methods=['POST'])
def sound_source():
data = request.json
renderer.set_sound_source(data['source'])
return 'OK'
@app.route('/sound/amp')
def sound_amp():
return {'amp': renderer.sound_amp}
@app.route('/static/sound.mp3')
def serve_sound():
if os.path.exists('sound.mp3'):
with open('sound.mp3', 'rb') as f:
return Response(f.read(), mimetype='audio/mpeg')
return 'Sound not found', 404
# ============ HTML PAGE ============
@app.route('/')
def index():
return render_template_string(f'''
<!DOCTYPE html>
<html>
<head>
<title>🎮 Pygame 720p Streaming</title>
<style>
body {{
margin: 0;
background: #0a0a0a;
color: white;
font-family: 'Segoe UI', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}}
.container {{
max-width: 1400px;
padding: 20px;
text-align: center;
}}
h1 {{
color: #4CAF50;
margin-bottom: 20px;
}}
.video-container {{
background: #000;
border-radius: 12px;
padding: 5px;
margin: 20px 0;
box-shadow: 0 0 30px rgba(76, 175, 80, 0.2);
position: relative;
}}
#videoPlayer, #mjpegImg {{
width: 100%;
max-width: {VIDEO_WIDTH}px;
height: auto;
border-radius: 8px;
display: block;
margin: 0 auto;
background: #111;
cursor: crosshair;
}}
#mjpegImg {{
display: none;
}}
.mouse-coords {{
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: #4CAF50;
padding: 5px 10px;
border-radius: 20px;
font-family: monospace;
font-size: 14px;
pointer-events: none;
}}
.controls {{
background: #1a1a1a;
border-radius: 12px;
padding: 20px;
margin-top: 20px;
}}
.format-buttons, .sound-controls {{
display: flex;
gap: 10px;
justify-content: center;
margin: 20px 0;
flex-wrap: wrap;
}}
button {{
background: #333;
color: white;
border: none;
padding: 12px 24px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
min-width: 120px;
font-weight: bold;
border: 1px solid #444;
}}
button:hover {{
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}}
button.active {{
background: #4CAF50;
border-color: #4CAF50;
box-shadow: 0 0 20px #4CAF50;
}}
.status-panel {{
background: #222;
border-radius: 8px;
padding: 15px;
margin-top: 20px;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 15px;
}}
.status-item {{
display: flex;
align-items: center;
gap: 10px;
}}
.status-label {{
color: #888;
font-size: 14px;
}}
.status-value {{
background: #333;
padding: 5px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
}}
.meter {{
width: 100%;
height: 25px;
background: #333;
border-radius: 12px;
overflow: hidden;
margin: 10px 0;
}}
.meter-fill {{
height: 100%;
width: 0%;
background: linear-gradient(90deg, #4CAF50, #2196F3);
transition: width 0.05s;
}}
</style>
</head>
<body>
<div class="container">
<h1>🎮 Pygame 720p Streaming</h1>
<div class="video-container">
<video id="videoPlayer" autoplay controls muted></video>
<img id="mjpegImg" crossorigin="anonymous">
<div id="mouseCoords" class="mouse-coords">X: 320, Y: 240</div>
</div>
<div class="controls">
<div class="format-buttons">
<button id="btnMjpeg" onclick="setFormat('mjpeg')" class="active">📸 MJPEG</button>
<button id="btnWebm" onclick="setFormat('webm')">🎥 WebM</button>
<button id="btnMp4" onclick="setFormat('mp4')">🎬 MP4</button>
</div>
<div class="sound-controls">
<button id="btnNone" onclick="setSound('none')" class="active">🔇 None</button>
<button id="btnPygame" onclick="setSound('pygame')">🎮 Pygame</button>
<button id="btnBrowser" onclick="setSound('browser')">🌐 Browser</button>
</div>
<div class="meter">
<div id="soundMeter" class="meter-fill"></div>
</div>
<div class="status-panel">
<div class="status-item">
<span class="status-label">Format:</span>
<span id="currentFormat" class="status-value">MJPEG</span>
</div>
<div class="status-item">
<span class="status-label">Sound:</span>
<span id="currentSource" class="status-value">None</span>
</div>
<div class="status-item">
<span class="status-label">Resolution:</span>
<span class="status-value">{VIDEO_WIDTH}x{VIDEO_HEIGHT}</span>
</div>
</div>
</div>
</div>
<audio id="browserAudio" loop style="display:none;">
<source src="/static/sound.mp3" type="audio/mpeg">
</audio>
<script>
const videoPlayer = document.getElementById('videoPlayer');
const mjpegImg = document.getElementById('mjpegImg');
const browserAudio = document.getElementById('browserAudio');
let currentFormat = 'mjpeg';
let currentSource = 'none';
let lastMouseSend = 0;
// Mouse tracking
function handleMouseMove(e) {{
const rect = (currentFormat === 'mjpeg' ? mjpegImg : videoPlayer).getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) * {VIDEO_WIDTH} / rect.width);
const y = Math.round((e.clientY - rect.top) * {VIDEO_HEIGHT} / rect.height);
document.getElementById('mouseCoords').innerHTML = `X: ${{x}}, Y: ${{y}}`;
const now = Date.now();
if (now - lastMouseSend > 33) {{
fetch('/mouse', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{x: x, y: y}})
}});
lastMouseSend = now;
}}
}}
// Click handling for button
function handleClick(e) {{
const rect = (currentFormat === 'mjpeg' ? mjpegImg : videoPlayer).getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) * {VIDEO_WIDTH} / rect.width);
const y = Math.round((e.clientY - rect.top) * {VIDEO_HEIGHT} / rect.height);
fetch('/click', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{x: x, y: y}})
}});
}}
videoPlayer.addEventListener('mousemove', handleMouseMove);
mjpegImg.addEventListener('mousemove', handleMouseMove);
videoPlayer.addEventListener('click', handleClick);
mjpegImg.addEventListener('click', handleClick);
// Sound handling
function setSound(source) {{
currentSource = source;
document.getElementById('btnNone').className = source === 'none' ? 'active' : '';
document.getElementById('btnPygame').className = source === 'pygame' ? 'active' : '';
document.getElementById('btnBrowser').className = source === 'browser' ? 'active' : '';
document.getElementById('currentSource').innerHTML =
source.charAt(0).toUpperCase() + source.slice(1);
if (source === 'browser') {{
browserAudio.play().catch(e => console.log('Audio error:', e));
}} else {{
browserAudio.pause();
browserAudio.currentTime = 0;
}}
fetch('/sound/source', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{source: source}})
}});
}}
// Sound meter
function updateSoundMeter() {{
fetch('/sound/amp')
.then(res => res.json())
.then(data => {{
document.getElementById('soundMeter').style.width = (data.amp * 100) + '%';
}});
setTimeout(updateSoundMeter, 100);
}}
updateSoundMeter();
// Format switching
function setFormat(format) {{
currentFormat = format;
document.getElementById('btnMjpeg').className = format === 'mjpeg' ? 'active' : '';
document.getElementById('btnWebm').className = format === 'webm' ? 'active' : '';
document.getElementById('btnMp4').className = format === 'mp4' ? 'active' : '';
document.getElementById('currentFormat').innerHTML = format.toUpperCase();
videoPlayer.style.display = 'none';
mjpegImg.style.display = 'none';
if (format === 'mjpeg') {{
mjpegImg.style.display = 'block';
mjpegImg.src = '/video/mjpeg?' + Date.now();
}} else {{
videoPlayer.style.display = 'block';
videoPlayer.src = `/video/${{format}}?` + Date.now();
videoPlayer.play().catch(e => console.log('Playback error:', e));
}}
}}
// Initialize
setFormat('mjpeg');
setSound('none');
</script>
</body>
</html>
''')
if __name__ == '__main__':
print("\n" + "="*70)
print("🎮 Pygame 720p Streaming App")
print("="*70)
print(f"📡 Resolution: {VIDEO_WIDTH}x{VIDEO_HEIGHT} @ {VIDEO_FPS}fps")
print("📡 Streaming endpoints:")
print(" • /video/mjpeg - MJPEG stream")
print(" • /video/webm - WebM stream")
print(" • /video/mp4 - MP4 stream")
print("🖱️ Interactive: mouse + clickable button")
print(f"\n🌐 Main page: /")
print("="*70 + "\n")
port = int(os.environ.get('PORT', STREAM_PORT))
app.run(host='0.0.0.0', port=port, debug=False, threaded=True)