Commit latest changes on experimental branch

parent 9ed29d92
......@@ -229,6 +229,12 @@ Examples:
help='Enable single window overlay mode (overlay stacks on top of player widget instead of separate window)'
)
parser.add_argument(
'--overlay-web',
action='store_true',
help='Enable web-based player mode (QWebEngineView with static HTML/JS player instead of native Qt player)'
)
return parser.parse_args()
def validate_arguments(args):
......@@ -241,6 +247,11 @@ def validate_arguments(args):
print("Error: Web port must be between 1 and 65535")
sys.exit(1)
# Ensure mutual exclusivity between overlay modes
if args.overlay_single and args.overlay_web:
print("Error: Cannot use both --overlay-single and --overlay-web options. They are mutually exclusive.")
sys.exit(1)
# Directory creation is handled by AppSettings.ensure_directories()
# which uses persistent user directories for PyInstaller compatibility
pass
......@@ -300,6 +311,7 @@ def main():
# Overlay settings
settings.qt.overlay_single_window = args.overlay_single
settings.qt.overlay_web_player = args.overlay_web
if args.db_path:
settings.database_path = args.db_path
......
......@@ -229,6 +229,7 @@ class QtConfig:
default_template: str = "news_template"
overlay_opacity: float = 0.9
overlay_single_window: bool = False
overlay_web_player: bool = False
# Performance settings
hardware_acceleration: bool = True
......
......@@ -140,6 +140,7 @@ class MbetterClientApplication:
# Preserve command line overlay settings
stored_settings.qt.overlay_single_window = self.settings.qt.overlay_single_window
stored_settings.qt.overlay_web_player = self.settings.qt.overlay_web_player
# Preserve command line SSL settings
stored_settings.web.enable_ssl = self.settings.web.enable_ssl
......
......@@ -55,6 +55,10 @@ class OverlayWebChannel(QObject):
# Signal to receive console messages from JavaScript
consoleMessage = pyqtSignal(str, str, int, str) # level, message, line, source
# Signals for web player communication
playVideo = pyqtSignal(str) # filePath
updateOverlayData = pyqtSignal(dict) # overlay data
def __init__(self, db_manager=None, message_bus=None):
super().__init__()
self.mutex = QMutex()
......@@ -245,6 +249,18 @@ class OverlayWebChannel(QObject):
logger.error(f"Failed to get timer state: {e}")
return json.dumps({"running": False, "remaining_seconds": 0})
@pyqtSlot(str)
def playVideo(self, filePath: str):
"""Send play video command to JavaScript (called by Qt)"""
logger.info(f"OverlayWebChannel.playVideo called with: {filePath}")
self.playVideo.emit(filePath)
@pyqtSlot(dict)
def updateOverlayData(self, data: dict):
"""Send overlay data update to JavaScript (called by Qt)"""
logger.debug(f"OverlayWebChannel.updateOverlayData called with: {data}")
self.updateOverlayData.emit(data)
@pyqtSlot(int, result=str)
def getWinningBets(self, match_id: int) -> str:
"""Provide winning bets data for a match to JavaScript via WebChannel"""
......@@ -1907,6 +1923,546 @@ class PlayerControlsWidget(QWidget):
return f"{minutes:02d}:{seconds:02d}"
class WebPlayerWindow(QMainWindow):
"""Web-based player window using QWebEngineView with static HTML/JS"""
# Signals for thread communication
position_changed = pyqtSignal(int, int)
video_loaded = pyqtSignal(str)
def __init__(self, settings: QtConfig, message_bus: MessageBus = None, debug_overlay: bool = False, qt_player=None):
super().__init__()
self.settings = settings
self.debug_overlay = debug_overlay
self.mutex = QMutex()
self.thread_pool = QThreadPool()
self.thread_pool.setMaxThreadCount(4)
self._message_bus = message_bus # Store message bus reference for shutdown messages
self.qt_player = qt_player # Reference to parent QtVideoPlayer for message sending
self.setup_ui()
self.setup_web_player()
self.setup_timers()
logger.info("WebPlayerWindow initialized")
def setup_ui(self):
"""Setup web player window UI"""
self.setWindowTitle("MbetterClient - Web Player")
# Clean interface
self.setMenuBar(None)
self.setStatusBar(None)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
# Set black background
from PyQt6.QtGui import QPalette
palette = self.palette()
palette.setColor(QPalette.ColorRole.Window, QColor(0, 0, 0))
palette.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
palette.setColor(QPalette.ColorRole.Base, QColor(0, 0, 0))
self.setPalette(palette)
self.setStyleSheet("QMainWindow { background-color: black; }")
# Central widget
central_widget = QWidget()
central_widget.setAutoFillBackground(True)
central_widget.setPalette(palette)
central_widget.setStyleSheet("background-color: black;")
self.setCentralWidget(central_widget)
# Layout
layout = QVBoxLayout(central_widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Create QWebEngineView for web player
self.web_player_view = QWebEngineView(central_widget)
layout.addWidget(self.web_player_view, 1)
# Window settings
if self.settings.fullscreen:
self.showFullScreen()
else:
self.resize(self.settings.window_width, self.settings.window_height)
self.show()
self.raise_()
self.activateWindow()
self.repaint()
if self.settings.always_on_top:
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
self.show()
def setup_web_player(self):
"""Setup web player with QWebEngineView"""
# Get web player HTML path
web_player_html_path = self._get_web_player_html_path()
if not web_player_html_path or not web_player_html_path.exists():
logger.error(f"Web player HTML not found: {web_player_html_path}")
# Load fallback
self._load_fallback_web_player()
return
logger.info(f"Loading web player from: {web_player_html_path}")
# Load the web player HTML
self.web_player_view.load(QUrl.fromLocalFile(str(web_player_html_path)))
# Setup WebChannel
self._setup_web_player_webchannel()
# Connect signals
self.web_player_view.page().loadFinished.connect(self._on_web_player_loaded)
def _get_web_player_html_path(self) -> Path:
"""Get path to web player HTML file"""
try:
# First try the web_player_assets directory
web_player_dir = Path(__file__).parent / "web_player_assets"
web_player_html = web_player_dir / "web_player.html"
if web_player_html.exists():
return web_player_html
# Fallback to templates directory (for backwards compatibility)
templates_dir = Path(__file__).parent / "templates"
fallback_html = templates_dir / "web_player.html"
if fallback_html.exists():
return fallback_html
logger.error("Web player HTML not found in any location")
return None
except Exception as e:
logger.error(f"Failed to determine web player HTML path: {e}")
return None
def _load_fallback_web_player(self):
"""Load fallback web player if main file not found"""
try:
fallback_html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mbetter Web Player - Fallback</title>
<style>
body {
margin: 0;
padding: 0;
background: black;
color: white;
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
}
.error-message {
background: rgba(255, 0, 0, 0.7);
padding: 20px;
border-radius: 10px;
max-width: 80%;
}
</style>
</head>
<body>
<div class="error-message">
<h1>Web Player Error</h1>
<p>Web player HTML file not found. Please check installation.</p>
</div>
</body>
</html>
"""
self.web_player_view.setHtml(fallback_html)
logger.error("Loaded fallback web player HTML")
except Exception as e:
logger.error(f"Failed to load fallback web player: {e}")
def _setup_web_player_webchannel(self):
"""Setup WebChannel for web player communication"""
try:
# Setup WebChannel
self.web_channel = QWebChannel()
self.web_player_channel = OverlayWebChannel(db_manager=self._get_database_manager(), message_bus=self._message_bus)
self.web_channel.registerObject("overlay", self.web_player_channel)
self.web_player_view.page().setWebChannel(self.web_channel)
# Connect WebChannel signals to handle video playback and overlay updates
self.web_player_channel.playVideo.connect(self._handle_web_player_play_video)
self.web_player_channel.updateOverlayData.connect(self._handle_web_player_update_overlay)
# Add WebChannel JavaScript to the page
self._inject_webchannel_javascript()
logger.info("WebChannel setup completed for web player")
except Exception as e:
logger.error(f"Failed to setup WebChannel for web player: {e}")
def _on_web_player_loaded(self, ok=None):
"""Handle web player load completion"""
try:
if ok is not None and not ok:
logger.warning("Web player failed to load")
return
logger.info("Web player loaded successfully")
# Inject WebChannel setup into the page
self._inject_webchannel_setup()
except Exception as e:
logger.error(f"Failed to handle web player load: {e}")
def _inject_webchannel_javascript(self):
"""Inject WebChannel JavaScript into the page"""
try:
# Inject QWebChannel JavaScript library
webchannel_js = """
// QWebChannel JavaScript library
var QWebChannel = (function() {
// Simplified QWebChannel implementation
function QWebChannel(transport, initCallback) {
this.transport = transport;
this.objects = {};
this.execCallbacks = {};
// Setup transport
if (transport) {
transport.onmessage = this.handleMessage.bind(this);
}
// Call initialization callback
if (initCallback) {
initCallback(this);
}
}
QWebChannel.prototype.handleMessage = function(message) {
try {
var data = JSON.parse(message.data);
if (data.type === 'response') {
if (this.execCallbacks[data.id]) {
this.execCallbacks[data.id](data.data);
delete this.execCallbacks[data.id];
}
} else if (data.type === 'signal') {
var objectName = data.object;
var signalName = data.signal;
var args = data.args;
if (this.objects[objectName] && this.objects[objectName][signalName]) {
this.objects[objectName][signalName].apply(this.objects[objectName], args);
}
}
} catch (e) {
console.error('Error handling WebChannel message:', e);
}
};
QWebChannel.prototype.call = function(objectName, methodName, args, callback) {
var id = Math.random().toString(36).substr(2, 9);
this.execCallbacks[id] = callback;
var message = {
type: 'call',
id: id,
object: objectName,
method: methodName,
args: args
};
this.transport.postMessage(JSON.stringify(message));
};
return QWebChannel;
})();
// Make QWebChannel available globally
window.QWebChannel = QWebChannel;
"""
self.web_player_view.page().runJavaScript(webchannel_js)
except Exception as e:
logger.error(f"Failed to inject WebChannel JavaScript: {e}")
def _inject_webchannel_setup(self):
"""Inject WebChannel setup into the web player page"""
try:
# Run JavaScript to setup WebChannel connection
setup_js = """
// Setup WebChannel connection
if (typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('WebChannel connected in web player');
// Connect to overlay object
if (channel.objects.overlay) {
window.overlay = channel.objects.overlay;
console.log('Web player overlay object connected');
// Set up data update handler
if (window.updateOverlayData) {
window.overlay.dataUpdated.connect(function(data) {
window.updateOverlayData(data);
});
}
// Set up video play handler
if (window.playVideo) {
window.overlay.playVideo.connect(function(filePath) {
console.log('WebChannel playVideo signal received:', filePath);
window.playVideo(filePath);
});
}
// Set up overlay data update handler
if (window.updateOverlayData) {
window.overlay.updateOverlayData.connect(function(data) {
console.log('WebChannel updateOverlayData signal received:', data);
window.updateOverlayData(data);
});
}
}
});
} else {
console.error('QWebChannel not available in web player');
}
"""
self.web_player_view.page().runJavaScript(setup_js)
except Exception as e:
logger.error(f"Failed to inject WebChannel setup: {e}")
def _handle_web_player_play_video(self, filePath: str):
"""Handle play video signal from WebChannel"""
try:
logger.info(f"WebPlayerWindow: Handling play video signal: {filePath}")
# This method is called when JavaScript signals to play a video
# For now, we can just log it since the video should already be playing
# through the direct JavaScript injection in play_video method
except Exception as e:
logger.error(f"Failed to handle web player play video: {e}")
def _handle_web_player_update_overlay(self, data: dict):
"""Handle overlay data update signal from WebChannel"""
try:
logger.info(f"WebPlayerWindow: Handling overlay update signal: {data}")
# This method is called when JavaScript signals to update overlay data
# For now, we can just log it since overlay updates are handled
# through the direct JavaScript injection in play_video method
except Exception as e:
logger.error(f"Failed to handle web player overlay update: {e}")
def _get_database_manager(self):
"""Get database manager from message bus"""
try:
if hasattr(self, '_message_bus') and self._message_bus:
# Try to get db_manager from web_dashboard component
try:
web_dashboard_queue = self._message_bus._queues.get('web_dashboard')
if web_dashboard_queue and hasattr(web_dashboard_queue, 'component'):
component = web_dashboard_queue.component
if hasattr(component, 'db_manager'):
logger.debug("WebPlayerWindow: Got db_manager from web_dashboard component")
return component.db_manager
except Exception as e:
logger.debug(f"WebPlayerWindow: Could not get db_manager from message bus: {e}")
# Fallback: create database manager directly
from ..config.settings import get_user_data_dir
from ..database.manager import DatabaseManager
db_path = get_user_data_dir() / "mbetterclient.db"
logger.debug(f"WebPlayerWindow: Creating database manager directly: {db_path}")
db_manager = DatabaseManager(str(db_path))
if db_manager.initialize():
return db_manager
else:
logger.warning("WebPlayerWindow: Failed to initialize database manager")
return None
except Exception as e:
logger.error(f"WebPlayerWindow: Failed to get database manager: {e}")
return None
def play_video(self, file_path: str, template_data: Dict[str, Any] = None, template_name: str = None, loop_data: Dict[str, Any] = None):
"""Play video file in web player"""
try:
logger.info(f"WebPlayerWindow.play_video() called with: {file_path}")
if not self.web_player_view:
logger.error("Web player view not available")
return
# Convert file path to URL for web player
file_url = QUrl.fromLocalFile(str(file_path)).toString()
logger.info(f"Converted file path to URL: {file_url}")
# Send play command to web player via WebChannel
if hasattr(self, 'web_player_channel') and self.web_player_channel:
# Update overlay data first
overlay_data = template_data or {}
overlay_data.update({
'title': f'Playing: {Path(file_path).name}',
'message': 'Web Player Active',
'videoPath': file_url
})
self.web_player_channel.updateOverlayData(overlay_data)
self.web_player_channel.playVideo(file_url)
logger.info("Sent video play command to web player")
else:
# Fallback: inject JavaScript to play video directly
play_js = f"""
if (window.webPlayer && window.webPlayer.playVideo) {{
window.webPlayer.playVideo('{file_url}');
}} else {{
console.error('WebPlayer.playVideo function not available in web player');
}}
"""
self.web_player_view.page().runJavaScript(play_js)
except Exception as e:
logger.error(f"Failed to play video in web player: {e}")
def setup_timers(self):
"""Setup timers for web player"""
# Overlay update timer
self.overlay_timer = QTimer()
self.overlay_timer.timeout.connect(self.update_overlay_periodically)
self.overlay_timer.start(1000) # Update every second
def get_media_player_state(self) -> int:
"""Get media player state for compatibility"""
return 1 # QMediaPlayer.PlaybackState.PlayingState equivalent
def get_media_player_position(self) -> int:
"""Get media player position for compatibility"""
return 0 # Default position
def get_media_player_duration(self) -> int:
"""Get media player duration for compatibility"""
return 0 # Default duration
@property
def media_player(self):
"""Mock media_player property for compatibility"""
# Return a mock object that has the expected properties and methods
class MockMediaPlayer:
def playbackState(self):
return 1 # QMediaPlayer.PlaybackState.PlayingState equivalent
def position(self):
return 0 # Default position
def duration(self):
return 0 # Default duration
def play(self):
pass
def pause(self):
pass
def stop(self):
pass
def setSource(self, url):
pass
def setVideoOutput(self, widget):
pass
def videoOutput(self):
return None
def error(self):
return None
def source(self):
return None
return MockMediaPlayer()
@property
def audio_output(self):
"""Mock audio_output property for compatibility"""
# Return a mock object that has the expected properties and methods
class MockAudioOutput:
def setVolume(self, volume):
pass
def volume(self):
return 1.0 # Default volume
def setMuted(self, muted):
pass
def isMuted(self):
return False
return MockAudioOutput()
def update_overlay_periodically(self):
"""Periodic overlay updates for web player"""
try:
current_time = time.strftime("%H:%M:%S")
if hasattr(self, 'web_player_channel'):
time_data = {
'currentTime': current_time,
'webServerBaseUrl': self._get_web_server_base_url()
}
self.web_player_channel.send_data_update(time_data)
except Exception as e:
logger.error(f"Periodic overlay update failed: {e}")
def _get_web_server_base_url(self) -> str:
"""Get the web server base URL for API requests"""
try:
# Try to get the URL from the parent QtVideoPlayer component
if hasattr(self, '_message_bus') and self._message_bus:
# Look for the qt_player component in the message bus
qt_player_queue = self._message_bus._queues.get('qt_player')
if qt_player_queue and hasattr(qt_player_queue, 'component'):
qt_player = qt_player_queue.component
if hasattr(qt_player, 'web_dashboard_url') and qt_player.web_dashboard_url:
logger.debug(f"Web server base URL from QtVideoPlayer: {qt_player.web_dashboard_url}")
return qt_player.web_dashboard_url
# Fallback to defaults if not available
base_url = "http://127.0.0.1:5001"
logger.debug(f"Web server base URL using fallback: {base_url}")
return base_url
except Exception as e:
logger.error(f"Failed to determine web server base URL: {e}")
return "http://127.0.0.1:5001"
def closeEvent(self, event):
"""Handle window close"""
with QMutexLocker(self.mutex):
self.overlay_timer.stop()
if hasattr(self, 'web_player_channel'):
# Clean up WebChannel
pass
logger.info("Web player window closing")
event.accept()
class PlayerWindow(QMainWindow):
"""Enhanced main player window with thread-safe operations"""
......@@ -3163,6 +3719,15 @@ class QtVideoPlayer(QObject):
self._configure_linux_app_settings()
# Create player window with message bus reference and debug settings
logger.info(f"DEBUG: overlay_web_player setting = {self.settings.overlay_web_player}")
logger.info(f"DEBUG: overlay_single_window setting = {self.settings.overlay_single_window}")
if self.settings.overlay_web_player:
# Use WebPlayerWindow for web-based player
logger.info("DEBUG: Creating WebPlayerWindow for web-based player")
self.window = WebPlayerWindow(self.settings, self.message_bus, debug_overlay=self.debug_overlay, qt_player=self)
else:
# Use standard PlayerWindow
logger.info("DEBUG: Creating standard PlayerWindow")
self.window = PlayerWindow(self.settings, self.message_bus, debug_overlay=self.debug_overlay, qt_player=self)
# CRITICAL: Connect signal to slot for cross-thread video playback
......
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mbetter Web Player</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: black;
overflow: hidden;
width: 100vw;
height: 100vh;
position: relative;
color: white;
}
/* Video container */
.video-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: black;
z-index: 100; /* Increased z-index to ensure video is above background but below overlays */
}
/* Video element */
#webVideoPlayer {
width: 100%;
height: 100%;
object-fit: contain;
background: black;
visibility: visible !important; /* Ensure video is visible */
opacity: 1 !important; /* Ensure video is not transparent */
}
/* Overlay container */
.overlay-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
/* Message panel */
.message-panel {
background: rgba(0, 123, 255, 0.40);
border-radius: 20px;
padding: 40px 60px;
min-width: 500px;
max-width: 80%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.2);
opacity: 0;
transform: translateY(-30px);
animation: slideInDown 1s ease-out forwards;
}
.message-title {
color: #ffffff;
font-size: 32px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.8);
opacity: 0;
animation: titleFadeIn 1.5s ease-out 0.5s forwards;
}
.message-content {
color: rgba(255, 255, 255, 0.95);
font-size: 20px;
text-align: center;
line-height: 1.6;
max-width: 100%;
word-wrap: break-word;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
opacity: 0;
animation: contentFadeIn 1.5s ease-out 1s forwards;
}
.message-icon {
font-size: 48px;
color: #ffffff;
margin-bottom: 20px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.8);
opacity: 0;
animation: iconBounce 2s ease-out 0.2s forwards;
}
/* Timer in top right */
.timer-container {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 123, 255, 0.8);
color: white;
padding: 12px 18px;
border-radius: 8px;
font-size: 32px;
font-weight: bold;
font-family: 'Courier New', monospace;
border: 2px solid rgba(255, 255, 255, 0.3);
z-index: 1001;
min-width: 140px;
text-align: center;
}
/* Bottom info bar */
.info-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 123, 255, 0.8);
backdrop-filter: blur(10px);
border-top: 2px solid rgba(255, 255, 255, 0.3);
padding: 20px;
z-index: 1001;
}
.fighter-names {
color: white;
font-size: 36px;
font-weight: bold;
text-align: center;
margin-bottom: 8px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.venue-info {
color: rgba(255, 255, 255, 0.95);
font-size: 28px;
text-align: center;
font-style: italic;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
/* Animations */
@keyframes slideInDown {
0% {
opacity: 0;
transform: translateY(-50px) scale(0.8);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes titleFadeIn {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes contentFadeIn {
0% {
opacity: 0;
transform: translateY(10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes iconBounce {
0% {
opacity: 0;
transform: scale(0.5);
}
50% {
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Responsive Design */
@media (max-width: 1200px) {
.message-panel {
padding: 30px 50px;
min-width: 400px;
}
.message-title {
font-size: 28px;
}
.message-content {
font-size: 18px;
}
.message-icon {
font-size: 40px;
}
.timer-container {
font-size: 28px;
padding: 10px 14px;
min-width: 130px;
}
}
@media (max-width: 800px) {
.message-panel {
padding: 25px 35px;
min-width: 90%;
max-width: 95%;
}
.message-title {
font-size: 24px;
margin-bottom: 15px;
}
.message-content {
font-size: 16px;
line-height: 1.5;
}
.message-icon {
font-size: 36px;
}
.timer-container {
font-size: 24px;
padding: 8px 12px;
top: 10px;
right: 10px;
min-width: 120px;
}
.info-bar {
padding: 15px;
}
}
</style>
</head>
<body>
<!-- Video container -->
<div class="video-container">
<video id="webVideoPlayer" controls autoplay playsinline>
<source src="" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<!-- Timer in top right -->
<div class="timer-container" id="matchTimer">
00:00
</div>
<!-- Overlay container -->
<div class="overlay-container">
<div class="message-panel" id="messagePanel">
<div class="message-icon" id="messageIcon">📢</div>
<div class="message-title" id="messageTitle">Townships Combat League</div>
<div class="message-content" id="messageContent">Waiting for game to start....</div>
</div>
</div>
<!-- Bottom info bar -->
<div class="info-bar">
<div class="fighter-names" id="fighterNames">Loading fighters...</div>
<div class="venue-info" id="venueInfo">Loading venue...</div>
</div>
<!-- Load WebPlayerAPI JavaScript -->
<script src="web_player.js"></script>
<!-- Qt WebChannel JavaScript will be injected by Qt -->
</body>
</html>
\ No newline at end of file
/**
* Web Player JavaScript API
* Provides communication between Qt WebChannel and the HTML5 video player
*/
// Web Player API
class WebPlayerAPI {
constructor() {
this.videoElement = null;
this.overlayData = {};
this.webChannel = null;
this.currentVideoPath = null;
this.timerInterval = null;
this.currentTime = 0;
}
// Initialize the web player
init() {
console.log('Initializing Web Player API');
// Get video element
this.videoElement = document.getElementById('webVideoPlayer');
if (!this.videoElement) {
console.error('Video element not found');
return false;
}
// Set up event listeners
this.setupEventListeners();
// Initialize WebChannel if available
this.initWebChannel();
return true;
}
// Set up event listeners
setupEventListeners() {
if (!this.videoElement) return;
this.videoElement.addEventListener('play', () => {
console.log('Video started playing');
this.startTimer();
this.sendPlayerStatus('playing');
});
this.videoElement.addEventListener('pause', () => {
console.log('Video paused');
this.stopTimer();
this.sendPlayerStatus('paused');
});
this.videoElement.addEventListener('ended', () => {
console.log('Video ended');
this.stopTimer();
this.sendPlayerStatus('ended');
});
this.videoElement.addEventListener('timeupdate', () => {
this.updateTimer();
});
this.videoElement.addEventListener('loadedmetadata', () => {
console.log('Video metadata loaded');
this.sendVideoInfo();
});
}
// Initialize WebChannel
initWebChannel() {
if (typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, (channel) => {
console.log('WebChannel initialized for web player');
this.webChannel = channel;
// Connect to overlay object if available
if (channel.objects.overlay) {
this.overlayChannel = channel.objects.overlay;
// Connect signals
if (this.overlayChannel.dataUpdated) {
this.overlayChannel.dataUpdated.connect((data) => {
this.handleOverlayData(data);
});
}
if (this.overlayChannel.playVideo) {
this.overlayChannel.playVideo.connect((filePath) => {
this.playVideo(filePath);
});
}
if (this.overlayChannel.updateOverlayData) {
this.overlayChannel.updateOverlayData.connect((data) => {
this.handleOverlayData(data);
});
}
// Get initial data
if (this.overlayChannel.getCurrentData) {
this.overlayChannel.getCurrentData((data) => {
this.handleOverlayData(data);
});
}
}
});
}
}
// Handle overlay data from Qt
handleOverlayData(data) {
console.log('Web player received overlay data:', data);
this.overlayData = data || {};
// Update UI with overlay data
this.updateUIFromOverlayData();
// Check for video play command
if (data && data.videoPath) {
this.playVideo(data.videoPath);
}
}
// Update UI from overlay data
updateUIFromOverlayData() {
// Update title, message, etc. from overlay data
if (this.overlayData.title) {
const titleElement = document.getElementById('messageTitle');
if (titleElement) {
titleElement.textContent = this.overlayData.title;
}
}
if (this.overlayData.message) {
const messageElement = document.getElementById('messageContent');
if (messageElement) {
messageElement.textContent = this.overlayData.message;
}
}
// Update fighter names and venue if available
if (this.overlayData.fighterNames) {
const fighterNamesElement = document.getElementById('fighterNames');
if (fighterNamesElement) {
fighterNamesElement.textContent = this.overlayData.fighterNames;
}
}
if (this.overlayData.venueInfo) {
const venueInfoElement = document.getElementById('venueInfo');
if (venueInfoElement) {
venueInfoElement.textContent = this.overlayData.venueInfo;
}
}
}
// Play video
playVideo(filePath) {
if (!this.videoElement) {
console.error('Video element not available');
return;
}
console.log('Playing video:', filePath);
this.currentVideoPath = filePath;
// Set video source and load
this.videoElement.src = filePath;
// Add event listeners for better debugging
this.videoElement.onloadeddata = () => {
console.log('Video data loaded, attempting to play...');
this._attemptPlayback();
};
this.videoElement.oncanplay = () => {
console.log('Video can play, attempting to play...');
this._attemptPlayback();
};
this.videoElement.onerror = (e) => {
console.error('Video element error:', e);
this.sendPlayerError('load_failed', 'Failed to load video source');
};
// Load the video
this.videoElement.load();
// Debug video element state
console.log('Video element after load:', this.videoElement);
console.log('Video readyState:', this.videoElement.readyState);
console.log('Video networkState:', this.videoElement.networkState);
console.log('Video error:', this.videoElement.error);
}
// Attempt playback with autoplay policy handling
_attemptPlayback() {
if (!this.videoElement) return;
// Check if video is ready to play
if (this.videoElement.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
console.log('Video ready, attempting playback...');
// Try to play with autoplay policy handling
this.videoElement.play().then(() => {
console.log('Playback started successfully');
}).catch(e => {
console.error('Playback failed (likely due to autoplay policy):', e);
// If autoplay is blocked, show controls and let user interact
this.videoElement.controls = true;
this.videoElement.muted = true; // Muted videos can often autoplay
// Try again with muted
this.videoElement.play().catch(e2 => {
console.error('Muted playback also failed:', e2);
this.sendPlayerError('autoplay_blocked', 'Autoplay blocked by browser policy');
});
});
} else {
console.log('Video not ready yet, waiting for more data...');
}
}
// Start timer
startTimer() {
this.stopTimer(); // Clear any existing timer
this.currentTime = 0;
this.updateTimerDisplay();
this.timerInterval = setInterval(() => {
this.currentTime++;
this.updateTimerDisplay();
}, 1000);
}
// Stop timer
stopTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
// Update timer display
updateTimerDisplay() {
const timerElement = document.getElementById('matchTimer');
if (timerElement) {
const minutes = Math.floor(this.currentTime / 60);
const seconds = this.currentTime % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
timerElement.textContent = timeString;
}
}
// Update timer based on video position
updateTimer() {
if (this.videoElement && !isNaN(this.videoElement.duration) && this.videoElement.duration > 0) {
this.currentTime = Math.floor(this.videoElement.currentTime);
this.updateTimerDisplay();
}
}
// Send player status to Qt
sendPlayerStatus(status) {
if (this.webChannel && this.overlayChannel && this.overlayChannel.sendPlayerStatus) {
this.overlayChannel.sendPlayerStatus(status);
}
}
// Send video info to Qt
sendVideoInfo() {
if (this.videoElement && this.webChannel && this.overlayChannel && this.overlayChannel.sendVideoInfo) {
const videoInfo = {
duration: this.videoElement.duration,
width: this.videoElement.videoWidth,
height: this.videoElement.videoHeight,
currentTime: this.videoElement.currentTime
};
this.overlayChannel.sendVideoInfo(videoInfo);
}
}
// Send player error to Qt
sendPlayerError(errorType, errorMessage) {
if (this.webChannel && this.overlayChannel && this.overlayChannel.sendPlayerError) {
this.overlayChannel.sendPlayerError(errorType, errorMessage);
}
}
// Get current player state
getPlayerState() {
if (!this.videoElement) return null;
return {
playing: !this.videoElement.paused,
currentTime: this.videoElement.currentTime,
duration: this.videoElement.duration,
volume: this.videoElement.volume,
muted: this.videoElement.muted,
videoPath: this.currentVideoPath
};
}
}
// Initialize the web player when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
const webPlayer = new WebPlayerAPI();
webPlayer.init();
// Expose to global scope for debugging
window.webPlayer = webPlayer;
// Expose functions globally for WebChannel communication
window.playVideo = function(filePath) {
console.log('Global playVideo called with:', filePath);
webPlayer.playVideo(filePath);
};
window.updateOverlayData = function(data) {
console.log('Global updateOverlayData called with:', data);
webPlayer.handleOverlayData(data);
};
console.log('Web Player API initialized and exposed to window.webPlayer');
console.log('Global functions playVideo and updateOverlayData exposed for WebChannel');
});
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<title>Simple Video Test</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: black;
overflow: hidden;
width: 100vw;
height: 100vh;
position: relative;
color: white;
}
/* Video container */
.video-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: black;
z-index: 100;
}
/* Video element */
#testVideo {
width: 100%;
height: 100%;
object-fit: contain;
background: black;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
}
/* Status overlay */
.status {
position: absolute;
top: 20px;
left: 20px;
color: white;
font-family: Arial, sans-serif;
font-size: 18px;
z-index: 1000;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 3px;
}
</style>
</head>
<body>
<div class="status" id="status">Loading video...</div>
<div class="video-container">
<video id="testVideo" controls autoplay playsinline muted>
<source src="file:///home/nextime/mbetterc/assets/INTRO.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<script>
// Debug video element
const video = document.getElementById('testVideo');
const status = document.getElementById('status');
console.log('Video element:', video);
console.log('Video style visibility:', window.getComputedStyle(video).visibility);
console.log('Video style opacity:', window.getComputedStyle(video).opacity);
console.log('Video style display:', window.getComputedStyle(video).display);
// Force video to be visible
video.style.visibility = 'visible';
video.style.opacity = '1';
video.style.display = 'block';
video.addEventListener('loadedmetadata', function() {
status.textContent = 'Video loaded: ' + video.duration + ' seconds';
console.log('Video metadata loaded');
});
video.addEventListener('play', function() {
status.textContent = 'Video playing';
console.log('Video started playing');
});
video.addEventListener('pause', function() {
status.textContent = 'Video paused';
console.log('Video paused');
});
video.addEventListener('ended', function() {
status.textContent = 'Video ended';
console.log('Video ended');
});
video.addEventListener('error', function(e) {
status.textContent = 'Video error: ' + e.message;
console.error('Video error:', e);
});
// Try to play the video
video.play().then(() => {
console.log('Playback started successfully');
}).catch(e => {
console.error('Playback failed:', e);
status.textContent = 'Playback failed: ' + e.message;
// If autoplay is blocked, show controls and let user interact
video.controls = true;
video.muted = true;
// Try again with muted
video.play().catch(e2 => {
console.error('Muted playback also failed:', e2);
});
});
</script>
</body>
</html>
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script for video playback functionality in the web player
"""
import sys
import time
import logging
from pathlib import Path
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QUrl, QTimer
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEngineProfile
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def test_video_playback():
"""Test video playback in a QWebEngineView"""
try:
logger.info("Testing video playback in QWebEngineView...")
# Create QApplication
app = QApplication(sys.argv)
# Create a simple web view
web_view = QWebEngineView()
web_view.setWindowTitle("Video Playback Test")
web_view.resize(800, 600)
# Get the path to the sample video
sample_video_path = Path(__file__).parent / "assets" / "INTRO.mp4"
if not sample_video_path.exists():
# Try to find any video file
assets_dir = Path(__file__).parent / "assets"
video_files = list(assets_dir.glob("*.mp4"))
if video_files:
sample_video_path = video_files[0]
else:
logger.error("No video files found for testing")
return False
# Create HTML content with video player
video_url = QUrl.fromLocalFile(str(sample_video_path)).toString()
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>Video Playback Test</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
margin: 0;
padding: 0;
background: black;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
overflow: hidden;
}}
.video-container {{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: black;
z-index: 100;
}}
video {{
width: 100%;
height: 100%;
object-fit: contain;
background: black;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
}}
.status {{
position: absolute;
top: 20px;
left: 20px;
color: white;
font-family: Arial, sans-serif;
font-size: 18px;
z-index: 1000;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 3px;
}}
</style>
</head>
<body>
<div class="status" id="status">Loading video...</div>
<div class="video-container">
<video id="testVideo" controls autoplay playsinline>
<source src="{video_url}" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<script>
// Debug video element
const video = document.getElementById('testVideo');
const status = document.getElementById('status');
console.log('Video element:', video);
console.log('Video style visibility:', window.getComputedStyle(video).visibility);
console.log('Video style opacity:', window.getComputedStyle(video).opacity);
console.log('Video style display:', window.getComputedStyle(video).display);
// Force video to be visible
video.style.visibility = 'visible';
video.style.opacity = '1';
video.style.display = 'block';
video.addEventListener('loadedmetadata', function() {{
status.textContent = 'Video loaded: ' + video.duration + ' seconds';
console.log('Video metadata loaded');
}});
video.addEventListener('play', function() {{
status.textContent = 'Video playing';
console.log('Video started playing');
}});
video.addEventListener('pause', function() {{
status.textContent = 'Video paused';
console.log('Video paused');
}});
video.addEventListener('ended', function() {{
status.textContent = 'Video ended';
console.log('Video ended');
}});
video.addEventListener('error', function(e) {{
status.textContent = 'Video error: ' + e.message;
console.error('Video error:', e);
}});
// Try to play the video
video.play().catch(function(e) {{
status.textContent = 'Playback failed: ' + e.message;
console.error('Playback failed:', e);
}});
</script>
</body>
</html>
"""
# Load the HTML content
web_view.setHtml(html_content)
web_view.show()
logger.info(f"Testing video playback with: {sample_video_path}")
logger.info("Web view should appear with video playback...")
# Run the application for a short time to test
app.exec()
return True
except Exception as e:
logger.error(f"Video playback test failed: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return False
if __name__ == "__main__":
success = test_video_playback()
sys.exit(0 if success else 1)
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script for the new web player functionality
"""
import sys
import time
import logging
from pathlib import Path
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def test_web_player():
"""Test the web player functionality"""
try:
# Import the main application
from main import main
from mbetterclient.config.settings import QtConfig
from mbetterclient.core.message_bus import MessageBus
from mbetterclient.qt_player.player import QtVideoPlayer
logger.info("Testing web player functionality...")
# Test 1: Check if the --overlay-web option is properly recognized
test_args = [
'--overlay-web',
'--debug',
'--no-fullscreen',
'--no-screen-cast'
]
logger.info(f"Test arguments: {test_args}")
# Test 2: Verify that the web player HTML file exists
web_player_html_path = Path(__file__).parent / "mbetterclient" / "qt_player" / "web_player_assets" / "web_player.html"
if web_player_html_path.exists():
logger.info(f"✓ Web player HTML file found: {web_player_html_path}")
else:
logger.error(f"✗ Web player HTML file not found: {web_player_html_path}")
return False
# Test 3: Verify that the web player JS file exists
web_player_js_path = Path(__file__).parent / "mbetterclient" / "qt_player" / "web_player_assets" / "web_player.js"
if web_player_js_path.exists():
logger.info(f"✓ Web player JS file found: {web_player_js_path}")
else:
logger.error(f"✗ Web player JS file not found: {web_player_js_path}")
return False
# Test 4: Test video playback with a sample video
sample_video_path = Path(__file__).parent / "assets" / "INTRO.mp4"
if sample_video_path.exists():
logger.info(f"✓ Sample video found: {sample_video_path}")
else:
logger.warning(f"✗ Sample video not found: {sample_video_path}")
# Try to find any video file
assets_dir = Path(__file__).parent / "assets"
video_files = list(assets_dir.glob("*.mp4"))
if video_files:
sample_video_path = video_files[0]
logger.info(f"✓ Found alternative video: {sample_video_path}")
else:
logger.warning("No video files found in assets directory")
sample_video_path = None
logger.info("✓ All web player tests passed!")
return True
except Exception as e:
logger.error(f"Web player test failed: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return False
if __name__ == "__main__":
success = test_web_player()
sys.exit(0 if success else 1)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment