Upload files to "/"
Add core Python modules
This commit is contained in:
911
web_interface.py
Normal file
911
web_interface.py
Normal file
@@ -0,0 +1,911 @@
|
||||
# web_interface_clean.py
|
||||
"""
|
||||
Clean Web Interface - No Logs, Fixed Status Updates
|
||||
Focuses on controls and statistics with better real-time updates
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from flask_socketio import SocketIO, emit
|
||||
import queue
|
||||
|
||||
class DateTimeJSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, timedelta):
|
||||
return str(obj)
|
||||
return super().default(obj)
|
||||
|
||||
def make_json_safe(data):
|
||||
if isinstance(data, dict):
|
||||
return {key: make_json_safe(value) for key, value in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [make_json_safe(item) for item in data]
|
||||
elif isinstance(data, datetime):
|
||||
return data.isoformat()
|
||||
elif isinstance(data, timedelta):
|
||||
return str(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
class WebInterface:
|
||||
def __init__(self, debug_console=None, host='0.0.0.0', port=8547):
|
||||
self.debug_console = debug_console
|
||||
self.video_player = None
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Web command tracking
|
||||
self.command_history = []
|
||||
self.max_history = 100
|
||||
self.command_lock = threading.Lock()
|
||||
|
||||
# Statistics cache with shorter refresh
|
||||
self.stats_cache = {}
|
||||
self.last_stats_update = 0
|
||||
self.stats_cache_duration = 0.5 # Update every 500ms for better responsiveness
|
||||
|
||||
# Message queues
|
||||
self.message_queue = queue.Queue()
|
||||
self.log_queue = queue.Queue()
|
||||
|
||||
# Flask app setup
|
||||
self.app = Flask(__name__)
|
||||
self.app.config['SECRET_KEY'] = 'video_player_secret_key_2024'
|
||||
|
||||
# SocketIO setup
|
||||
self.socketio = SocketIO(
|
||||
self.app,
|
||||
cors_allowed_origins="*",
|
||||
json=DateTimeJSONEncoder,
|
||||
async_mode='threading'
|
||||
)
|
||||
|
||||
# Background task control
|
||||
self.running = True
|
||||
self.stats_thread = None
|
||||
self.log_thread = None
|
||||
|
||||
# Setup routes and handlers
|
||||
self.setup_routes()
|
||||
self.setup_socketio_handlers()
|
||||
|
||||
self.logger.info("Clean web interface initialized successfully")
|
||||
|
||||
# =================================================================
|
||||
# REQUIRED COMPATIBILITY METHODS
|
||||
# =================================================================
|
||||
|
||||
def log(self, message, level="INFO"):
|
||||
"""Required by existing debug console"""
|
||||
try:
|
||||
if level == "ERROR":
|
||||
self.logger.error(message)
|
||||
elif level == "WARNING":
|
||||
self.logger.warning(message)
|
||||
else:
|
||||
self.logger.info(message)
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).error(f"Web interface log error: {e}")
|
||||
|
||||
def log_error(self, message):
|
||||
self.log(message, "ERROR")
|
||||
|
||||
def log_info(self, message):
|
||||
self.log(message, "INFO")
|
||||
|
||||
def log_warning(self, message):
|
||||
self.log(message, "WARNING")
|
||||
|
||||
def log_video_played(self, video_name):
|
||||
"""Required compatibility method"""
|
||||
self.log(f"Video played: {video_name}", "INFO")
|
||||
self.log_web_command("VIDEO_PLAYED",
|
||||
params={'video': video_name},
|
||||
source="player",
|
||||
details=f"Video started: {video_name}")
|
||||
|
||||
# Force immediate stats update when video changes
|
||||
self.last_stats_update = 0
|
||||
self.stats_cache = {}
|
||||
|
||||
def set_video_player(self, video_player):
|
||||
"""Required by main application"""
|
||||
self.video_player = video_player
|
||||
self.log(f"Video player connected to web interface")
|
||||
|
||||
def update_global_input_status(self, *args, **kwargs):
|
||||
"""Required by debug console - flexible parameters"""
|
||||
pass
|
||||
|
||||
def get_stats(self):
|
||||
"""Alternative method name for getting statistics"""
|
||||
return self.get_current_stats()
|
||||
|
||||
# =================================================================
|
||||
# CORE METHODS - IMPROVED STATUS TRACKING
|
||||
# =================================================================
|
||||
|
||||
def log_web_command(self, command, params=None, source="web", success=True, details=None):
|
||||
"""Comprehensive logging for all web commands"""
|
||||
try:
|
||||
timestamp = datetime.now()
|
||||
|
||||
log_entry = {
|
||||
'timestamp': timestamp.isoformat(),
|
||||
'command': command,
|
||||
'source': source,
|
||||
'success': success,
|
||||
'params': params or {},
|
||||
'details': details or 'Command executed'
|
||||
}
|
||||
|
||||
with self.command_lock:
|
||||
self.command_history.append(log_entry)
|
||||
if len(self.command_history) > self.max_history:
|
||||
self.command_history.pop(0)
|
||||
|
||||
status = "SUCCESS" if success else "FAILED"
|
||||
param_str = f" | Params: {params}" if params else ""
|
||||
detail_str = f" | {details}" if details else ""
|
||||
|
||||
log_message = f"[WEB-CMD] {command} [{status}] from {source.upper()}{param_str}{detail_str}"
|
||||
self.logger.info(log_message)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error logging web command: {e}")
|
||||
|
||||
def get_current_stats(self):
|
||||
"""Get current statistics with improved real-time updates"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# Use cached stats if very recent
|
||||
if (current_time - self.last_stats_update) < self.stats_cache_duration and self.stats_cache:
|
||||
return self.stats_cache
|
||||
|
||||
# Get fresh stats from debug console
|
||||
raw_stats = {}
|
||||
if self.debug_console:
|
||||
try:
|
||||
if hasattr(self.debug_console, 'get_stats'):
|
||||
raw_stats = self.debug_console.get_stats()
|
||||
elif hasattr(self.debug_console, 'stats'):
|
||||
raw_stats = dict(self.debug_console.stats)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error getting debug console stats: {e}")
|
||||
|
||||
# Calculate uptime
|
||||
uptime_str = '00:00:00'
|
||||
if 'start_time' in raw_stats and isinstance(raw_stats['start_time'], datetime):
|
||||
uptime = datetime.now() - raw_stats['start_time']
|
||||
uptime_str = str(uptime).split('.')[0]
|
||||
|
||||
# Get current status with better detection
|
||||
current_status = self.get_current_status()
|
||||
current_video = self.get_current_video()
|
||||
|
||||
# Build stats object
|
||||
safe_stats = {
|
||||
'videos_played': raw_stats.get('videos_played', 0),
|
||||
'nfc_scans': raw_stats.get('key_presses', 0),
|
||||
'errors': raw_stats.get('errors', 0),
|
||||
'uptime': uptime_str,
|
||||
'queue_depth': raw_stats.get('queue_depth', 0),
|
||||
'current_video': current_video,
|
||||
'status': current_status,
|
||||
'fullscreen': raw_stats.get('fullscreen', True),
|
||||
'web_commands_executed': len(self.command_history),
|
||||
'last_update': datetime.now().isoformat(),
|
||||
'connection_status': 'Connected' if self.debug_console else 'No Player',
|
||||
'is_playing': self.is_video_playing(),
|
||||
'player_ready': bool(self.get_video_player())
|
||||
}
|
||||
|
||||
# Cache the stats
|
||||
self.stats_cache = safe_stats
|
||||
self.last_stats_update = current_time
|
||||
|
||||
return safe_stats
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting stats: {e}")
|
||||
return {
|
||||
'videos_played': 0,
|
||||
'nfc_scans': 0,
|
||||
'errors': 1,
|
||||
'uptime': '00:00:00',
|
||||
'queue_depth': 0,
|
||||
'current_video': 'Error',
|
||||
'status': 'Error getting stats',
|
||||
'fullscreen': True,
|
||||
'web_commands_executed': 0,
|
||||
'last_update': datetime.now().isoformat(),
|
||||
'connection_status': 'Error',
|
||||
'is_playing': False,
|
||||
'player_ready': False
|
||||
}
|
||||
|
||||
def get_current_status(self):
|
||||
"""Get current status with better detection"""
|
||||
try:
|
||||
player = self.get_video_player()
|
||||
if not player:
|
||||
return "No video player connected"
|
||||
|
||||
# Check if specific video is playing
|
||||
if hasattr(player, 'specific_video_playing') and player.specific_video_playing:
|
||||
return "Playing specific video"
|
||||
|
||||
# Check if any video is playing
|
||||
if hasattr(player, 'is_video_playing'):
|
||||
try:
|
||||
if player.is_video_playing():
|
||||
return "Playing trailer"
|
||||
else:
|
||||
return "Ready"
|
||||
except:
|
||||
return "Ready"
|
||||
|
||||
# Check if video player is running
|
||||
if hasattr(player, 'running') and player.running:
|
||||
return "Ready"
|
||||
|
||||
return "Player stopped"
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error getting status: {e}")
|
||||
return "Status unknown"
|
||||
|
||||
def get_current_video(self):
|
||||
"""Get current video with better detection"""
|
||||
try:
|
||||
player = self.get_video_player()
|
||||
if not player:
|
||||
return "No video player"
|
||||
|
||||
# Try to get current video path
|
||||
if hasattr(player, 'current_video_path') and player.current_video_path:
|
||||
video_path = Path(player.current_video_path)
|
||||
return video_path.name
|
||||
|
||||
# Try to get from stats
|
||||
if self.debug_console and hasattr(self.debug_console, 'stats'):
|
||||
current_video = self.debug_console.stats.get('current_video', 'None')
|
||||
if current_video and current_video != 'None':
|
||||
return current_video
|
||||
|
||||
return "No video"
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error getting current video: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def is_video_playing(self):
|
||||
"""Check if video is currently playing"""
|
||||
try:
|
||||
player = self.get_video_player()
|
||||
if not player:
|
||||
return False
|
||||
|
||||
if hasattr(player, 'is_video_playing'):
|
||||
return player.is_video_playing()
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error checking if video playing: {e}")
|
||||
return False
|
||||
|
||||
def get_video_player(self):
|
||||
"""Get video player from multiple possible sources"""
|
||||
if self.video_player:
|
||||
return self.video_player
|
||||
elif self.debug_console and hasattr(self.debug_console, 'video_player'):
|
||||
return self.debug_console.video_player
|
||||
else:
|
||||
return None
|
||||
|
||||
def broadcast_stats(self):
|
||||
"""Broadcast statistics update via SocketIO"""
|
||||
try:
|
||||
safe_stats = self.get_current_stats()
|
||||
if hasattr(self, 'socketio'):
|
||||
self.socketio.emit('stats_update', safe_stats)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to broadcast stats: {e}")
|
||||
|
||||
def stats_broadcast_loop(self):
|
||||
"""Background thread to broadcast stats updates - faster updates"""
|
||||
while self.running:
|
||||
try:
|
||||
self.broadcast_stats()
|
||||
time.sleep(1) # Update every 1 second for better responsiveness
|
||||
except Exception as e:
|
||||
self.logger.error(f"Stats broadcast loop error: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def log_broadcast_loop(self):
|
||||
"""Background thread - not needed for clean interface but kept for compatibility"""
|
||||
while self.running:
|
||||
try:
|
||||
# Clear the queue but don't broadcast logs
|
||||
while not self.log_queue.empty():
|
||||
try:
|
||||
self.log_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Log broadcast loop error: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
def setup_routes(self):
|
||||
"""Setup Flask routes"""
|
||||
|
||||
@self.app.route('/')
|
||||
def index():
|
||||
"""Main interface"""
|
||||
try:
|
||||
self.log_web_command("PAGE_LOAD", source="web", details="Main interface accessed")
|
||||
return self.create_clean_html()
|
||||
except Exception as e:
|
||||
self.log_web_command("PAGE_LOAD", success=False, details=f"Error: {e}")
|
||||
return self.create_clean_html()
|
||||
|
||||
@self.app.route('/api/video/skip', methods=['POST'])
|
||||
def api_skip_video():
|
||||
"""Skip current video"""
|
||||
try:
|
||||
self.log_web_command("SKIP_VIDEO", source="web", details="User clicked skip button")
|
||||
|
||||
if self.get_video_player():
|
||||
self.get_video_player().skip_current_video()
|
||||
self.log_web_command("SKIP_VIDEO", success=True, details="Skip command sent to player")
|
||||
|
||||
# Force immediate stats update
|
||||
self.last_stats_update = 0
|
||||
|
||||
return jsonify({'success': True, 'message': 'Video skipped successfully'})
|
||||
else:
|
||||
self.log_web_command("SKIP_VIDEO", success=False, details="No video player available")
|
||||
return jsonify({'success': False, 'message': 'Video player not available'}), 500
|
||||
|
||||
except Exception as e:
|
||||
self.log_web_command("SKIP_VIDEO", success=False, details=f"Error: {str(e)}")
|
||||
return jsonify({'success': False, 'message': f'Skip failed: {str(e)}'}), 500
|
||||
|
||||
@self.app.route('/api/video/toggle-fullscreen', methods=['POST'])
|
||||
def api_toggle_fullscreen():
|
||||
"""Toggle fullscreen"""
|
||||
try:
|
||||
self.log_web_command("TOGGLE_FULLSCREEN", source="web", details="User toggled fullscreen")
|
||||
|
||||
if self.get_video_player():
|
||||
self.get_video_player().toggle_fullscreen()
|
||||
self.log_web_command("TOGGLE_FULLSCREEN", success=True, details="Fullscreen toggle sent to player")
|
||||
return jsonify({'success': True, 'message': 'Fullscreen toggled'})
|
||||
else:
|
||||
self.log_web_command("TOGGLE_FULLSCREEN", success=False, details="No video player available")
|
||||
return jsonify({'success': False, 'message': 'Video player not available'}), 500
|
||||
|
||||
except Exception as e:
|
||||
self.log_web_command("TOGGLE_FULLSCREEN", success=False, details=f"Error: {str(e)}")
|
||||
return jsonify({'success': False, 'message': f'Fullscreen toggle failed: {str(e)}'}), 500
|
||||
|
||||
@self.app.route('/api/nfc/send', methods=['POST'])
|
||||
def api_send_nfc():
|
||||
"""Send NFC input"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
nfc_id = str(data.get('nfc_id', '')).strip()
|
||||
|
||||
self.log_web_command("NFC_INPUT", params={'nfc_id': nfc_id},
|
||||
source="web", details=f"User sent NFC ID: {nfc_id}")
|
||||
|
||||
if self.get_video_player():
|
||||
self.get_video_player().play_specific_video(nfc_id)
|
||||
self.log_web_command("NFC_INPUT", params={'nfc_id': nfc_id},
|
||||
success=True, details=f"NFC {nfc_id} sent to player")
|
||||
|
||||
# Force immediate stats update
|
||||
self.last_stats_update = 0
|
||||
|
||||
return jsonify({'success': True, 'message': f'NFC {nfc_id} sent successfully'})
|
||||
else:
|
||||
self.log_web_command("NFC_INPUT", success=False, details="No video player available")
|
||||
return jsonify({'success': False, 'message': 'Video player not available'}), 500
|
||||
|
||||
except Exception as e:
|
||||
self.log_web_command("NFC_INPUT", params={'nfc_id': nfc_id if 'nfc_id' in locals() else 'unknown'},
|
||||
success=False, details=f"Error: {str(e)}")
|
||||
return jsonify({'success': False, 'message': f'NFC input failed: {str(e)}'}), 500
|
||||
|
||||
@self.app.route('/api/stats', methods=['GET'])
|
||||
def api_get_stats():
|
||||
"""Get statistics"""
|
||||
try:
|
||||
stats = self.get_current_stats()
|
||||
return jsonify(stats)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Stats API error: {e}")
|
||||
return jsonify({'error': 'Stats unavailable'}), 500
|
||||
|
||||
def setup_socketio_handlers(self):
|
||||
"""Setup SocketIO event handlers"""
|
||||
|
||||
@self.socketio.on('connect')
|
||||
def handle_connect():
|
||||
"""Handle client connection"""
|
||||
self.logger.info("Web client connected")
|
||||
self.log_web_command("CONNECT", source="socketio", details="Client connected")
|
||||
|
||||
# Send initial stats immediately
|
||||
emit('stats_update', self.get_current_stats())
|
||||
|
||||
@self.socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
"""Handle client disconnection"""
|
||||
self.logger.info("Web client disconnected")
|
||||
self.log_web_command("DISCONNECT", source="socketio", details="Client disconnected")
|
||||
|
||||
def create_clean_html(self):
|
||||
"""Create clean HTML interface without logs"""
|
||||
return '''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Video Player Controller</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(45deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin: 15px 0;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: linear-gradient(45deg, #4CAF50, #45a049);
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: linear-gradient(45deg, #f44336, #d32f2f);
|
||||
box-shadow: 0 4px 15px rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.status.info {
|
||||
background: linear-gradient(45deg, #2196F3, #1976D2);
|
||||
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
margin: 15px 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.input-group input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
box-shadow: 0 0 10px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.stat {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.stat:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.2rem;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.current-video {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.current-video-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #FFD700;
|
||||
margin-top: 10px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.connection-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.connected { background: #4CAF50; }
|
||||
.disconnected { background: #f44336; }
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.controls-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.4rem;
|
||||
color: #FFD700;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container { padding: 10px; }
|
||||
.panel { padding: 15px; }
|
||||
.header h1 { font-size: 2rem; }
|
||||
.stats-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎬 Video Player Controller</h1>
|
||||
<div id="status" class="status info">
|
||||
<span id="connectionIndicator" class="connection-indicator disconnected"></span>
|
||||
Connecting to video player...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>📊 Live Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="videosPlayed">0</div>
|
||||
<div class="stat-label">Videos Played</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="nfcScans">0</div>
|
||||
<div class="stat-label">NFC Scans</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="uptime">00:00:00</div>
|
||||
<div class="stat-label">Uptime</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="playerStatus">Unknown</div>
|
||||
<div class="stat-label">Player Status</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="current-video">
|
||||
<h3>🎥 Currently Playing</h3>
|
||||
<div class="current-video-value" id="currentVideo">No video</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>🎮 Video Controls</h3>
|
||||
<div class="controls-grid">
|
||||
<button class="btn" onclick="skipVideo()">⏭️ Skip Video</button>
|
||||
<button class="btn" onclick="toggleFullscreen()">🔳 Toggle Fullscreen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>🏷️ NFC Input</h3>
|
||||
<div class="input-group">
|
||||
<input type="text" id="nfcInput" placeholder="Enter NFC ID and press Enter..." onkeypress="handleNFCKeypress(event)">
|
||||
<button class="btn" onclick="sendNFC()">Send NFC</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
let lastStatsUpdate = 0;
|
||||
|
||||
function showStatus(message, type = 'info', duration = 3000) {
|
||||
const status = document.getElementById('status');
|
||||
const indicator = document.getElementById('connectionIndicator');
|
||||
|
||||
status.innerHTML = `<span id="connectionIndicator" class="connection-indicator ${type === 'success' ? 'connected' : 'disconnected'}"></span>${message}`;
|
||||
status.className = `status ${type}`;
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
status.innerHTML = '<span id="connectionIndicator" class="connection-indicator connected"></span>Ready';
|
||||
status.className = 'status success';
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
function skipVideo() {
|
||||
fetch('/api/video/skip', {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showStatus(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
// Request immediate stats update
|
||||
requestStatsUpdate();
|
||||
}
|
||||
})
|
||||
.catch(e => showStatus('Network error', 'error'));
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
fetch('/api/video/toggle-fullscreen', {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(data => showStatus(data.message, data.success ? 'success' : 'error'))
|
||||
.catch(e => showStatus('Network error', 'error'));
|
||||
}
|
||||
|
||||
function sendNFC() {
|
||||
const nfcId = document.getElementById('nfcInput').value.trim();
|
||||
if (!nfcId) return showStatus('Please enter NFC ID', 'error');
|
||||
|
||||
fetch('/api/nfc/send', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({nfc_id: nfcId})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showStatus(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
document.getElementById('nfcInput').value = '';
|
||||
// Request immediate stats update
|
||||
requestStatsUpdate();
|
||||
}
|
||||
})
|
||||
.catch(e => showStatus('Network error', 'error'));
|
||||
}
|
||||
|
||||
function handleNFCKeypress(event) {
|
||||
if (event.key === 'Enter') sendNFC();
|
||||
}
|
||||
|
||||
function requestStatsUpdate() {
|
||||
// Request fresh stats from server
|
||||
fetch('/api/stats')
|
||||
.then(r => r.json())
|
||||
.then(updateStats)
|
||||
.catch(e => console.error('Stats request failed:', e));
|
||||
}
|
||||
|
||||
function updateStats(stats) {
|
||||
try {
|
||||
// Update all stat values
|
||||
document.getElementById('videosPlayed').textContent = stats.videos_played || 0;
|
||||
document.getElementById('nfcScans').textContent = stats.nfc_scans || 0;
|
||||
document.getElementById('uptime').textContent = stats.uptime || '00:00:00';
|
||||
document.getElementById('playerStatus').textContent = stats.status || 'Unknown';
|
||||
|
||||
// Update current video with better formatting
|
||||
const currentVideo = stats.current_video || 'No video';
|
||||
const videoElement = document.getElementById('currentVideo');
|
||||
if (currentVideo.length > 50) {
|
||||
videoElement.textContent = currentVideo.substring(0, 47) + '...';
|
||||
videoElement.title = currentVideo;
|
||||
} else {
|
||||
videoElement.textContent = currentVideo;
|
||||
videoElement.title = currentVideo;
|
||||
}
|
||||
|
||||
// Update connection status
|
||||
const isConnected = stats.player_ready && stats.connection_status === 'Connected';
|
||||
const indicator = document.getElementById('connectionIndicator');
|
||||
if (indicator) {
|
||||
indicator.className = `connection-indicator ${isConnected ? 'connected' : 'disconnected'}`;
|
||||
}
|
||||
|
||||
lastStatsUpdate = Date.now();
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error updating stats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Socket event handlers
|
||||
socket.on('connect', () => {
|
||||
showStatus('Connected to video player', 'success', 0);
|
||||
console.log('Connected to video player web interface');
|
||||
requestStatsUpdate();
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
showStatus('Disconnected from video player', 'error', 0);
|
||||
console.log('Disconnected from video player');
|
||||
});
|
||||
|
||||
socket.on('stats_update', updateStats);
|
||||
|
||||
// Request stats update every 5 seconds as backup
|
||||
setInterval(() => {
|
||||
if (Date.now() - lastStatsUpdate > 5000) {
|
||||
requestStatsUpdate();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Initial connection attempt
|
||||
showStatus('Attempting to connect...', 'info');
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
def run(self, host=None, port=None, debug=False, **kwargs):
|
||||
"""Fixed run method that accepts all possible parameters"""
|
||||
try:
|
||||
run_host = host or self.host
|
||||
run_port = port or self.port
|
||||
|
||||
self.logger.info(f"Starting clean web interface on {run_host}:{run_port}")
|
||||
|
||||
# Start background threads
|
||||
if not self.stats_thread or not self.stats_thread.is_alive():
|
||||
self.stats_thread = threading.Thread(target=self.stats_broadcast_loop, daemon=True)
|
||||
self.log_thread = threading.Thread(target=self.log_broadcast_loop, daemon=True)
|
||||
|
||||
self.stats_thread.start()
|
||||
self.log_thread.start()
|
||||
self.logger.info("Background threads started")
|
||||
|
||||
# Start the SocketIO server
|
||||
self.socketio.run(
|
||||
self.app,
|
||||
host=run_host,
|
||||
port=run_port,
|
||||
debug=False,
|
||||
use_reloader=False,
|
||||
allow_unsafe_werkzeug=True
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Web interface startup error: {e}")
|
||||
self.logger.info("Web interface failed to start but application can continue")
|
||||
|
||||
def stop(self):
|
||||
"""Stop the web interface"""
|
||||
self.logger.info("Stopping clean web interface...")
|
||||
self.running = False
|
||||
|
||||
if self.stats_thread and self.stats_thread.is_alive():
|
||||
self.stats_thread.join(timeout=2)
|
||||
if self.log_thread and self.log_thread.is_alive():
|
||||
self.log_thread.join(timeout=2)
|
||||
|
||||
self.logger.info("Clean web interface stopped")
|
||||
|
||||
# Helper functions for backward compatibility
|
||||
def create_web_interface(debug_console=None, host='0.0.0.0', port=8547):
|
||||
return WebInterface(debug_console, host, port)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
web = WebInterface()
|
||||
try:
|
||||
web.run()
|
||||
except KeyboardInterrupt:
|
||||
web.stop()
|
||||
Reference in New Issue
Block a user