Fix Qt player overlay window positioning

- Add window positioning synchronization for separate overlay windows
- Overlay window now correctly appears over main video player window
- Add move and resize event handlers to keep windows synchronized
- Add proper overlay window cleanup in closeEvent
- Separate top-level window approach successfully resolves Qt rendering conflicts
parent 54d1a139
...@@ -2,763 +2,63 @@ ...@@ -2,763 +2,63 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Video Overlay</title> <title>Video Overlay Test</title>
<style> <style>
* { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: rgba(0, 0, 0, 0) !important;
overflow: hidden;
width: 100vw;
height: 100vh;
position: relative;
/* CRITICAL: Ensure body is completely transparent to allow video through */
background-color: transparent !important;
}
html {
background: transparent !important;
background-color: transparent !important;
}
/* Debug indicator to verify CSS is loaded */
body::before {
content: 'Overlay v2.1 loaded';
position: absolute;
top: 5px;
left: 5px;
color: rgba(255,255,255,0.5);
font-size: 10px;
z-index: 9999;
}
.overlay-container {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; background: rgba(255, 0, 0, 0.5); /* Semi-transparent red to test visibility */
z-index: 1000; overflow: hidden;
/* CRITICAL: Ensure container has no background to allow video through */
background: transparent !important;
}
.title-main {
position: absolute;
top: 20%;
left: 50%;
transform: translateX(-50%);
font-size: 48px;
font-weight: bold;
color: white;
text-shadow:
2px 2px 4px rgba(0,0,0,0.8),
-1px -1px 0px rgba(0,0,0,0.5),
1px -1px 0px rgba(0,0,0,0.5),
-1px 1px 0px rgba(0,0,0,0.5),
1px 1px 0px rgba(0,0,0,0.5);
text-align: center;
opacity: 0;
animation: titleSlideIn 2s ease-out forwards;
max-width: 90%;
word-wrap: break-word;
} }
.title-subtitle { .test-overlay {
position: absolute; position: absolute;
top: 30%; top: 50%;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translate(-50%, -50%);
background: rgba(0, 255, 0, 0.9);
color: black;
padding: 30px;
font-size: 24px; font-size: 24px;
color: #ffffff; font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.7); border: 5px solid yellow;
text-align: center; text-align: center;
opacity: 0;
animation: titleSlideIn 2s ease-out 0.5s forwards;
max-width: 90%;
} }
.news-ticker { .corner-indicator {
position: absolute; position: absolute;
bottom: 10%; top: 10px;
width: 100%; left: 10px;
background: linear-gradient(90deg, background: blue;
rgba(220, 53, 69, 0.9) 0%,
rgba(220, 53, 69, 0.95) 50%,
rgba(220, 53, 69, 0.9) 100%);
color: white;
padding: 12px 0;
font-size: 18px;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
opacity: 0;
animation: fadeIn 1s ease-in 1s forwards;
}
.ticker-text {
display: inline-block;
animation: scroll 30s linear infinite;
padding-left: 100%;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
.progress-bar {
position: absolute;
bottom: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
opacity: 0.8;
}
.logo {
position: absolute;
top: 20px;
left: 20px;
width: 80px;
height: 80px;
opacity: 0;
animation: logoSlideIn 1.5s ease-out 1.5s forwards;
}
.logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.stats-panel {
position: absolute;
top: 50%;
right: 20px;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.7);
border-radius: 10px;
padding: 20px;
color: white; color: white;
padding: 10px;
font-size: 16px; font-size: 16px;
opacity: 0;
animation: slideInRight 1s ease-out 2.5s forwards;
min-width: 200px;
}
.stats-item {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.canvas-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 500;
}
/* Animations */
@keyframes titleSlideIn {
0% {
opacity: 0;
transform: translateX(-50%) translateY(-50px);
}
100% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
@keyframes logoSlideIn {
0% {
opacity: 0;
transform: translateX(-100px);
}
100% {
opacity: 0.9;
transform: translateX(0);
}
}
@keyframes slideInRight {
0% {
opacity: 0;
transform: translateY(-50%) translateX(100px);
}
100% {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
/* Responsive Design */
@media (max-width: 1200px) {
.title-main {
font-size: 36px;
}
.title-subtitle {
font-size: 20px;
}
.news-ticker {
font-size: 16px;
}
}
@media (max-width: 800px) {
.title-main {
font-size: 28px;
}
.title-subtitle {
font-size: 16px;
}
.stats-panel {
right: 10px;
font-size: 14px;
padding: 15px;
}
} }
</style> </style>
</head> </head>
<body> <body>
<!-- CRITICAL: Minimal overlay structure to allow video to show through empty areas --> <div class="corner-indicator">
WebEngine Active
<!-- Logo - positioned but with transparent background -->
<div class="logo" id="logo">
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiByeD0iMTAiIGZpbGw9IiNkYzM1NDUiLz4KPHRleHQgeD0iNDAiIHk9IjQ1IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zNWVtIj5NPC90ZXh0Pgo8L3N2Zz4K" alt="MbetterClient Logo" onerror="this.style.display='none'">
</div>
<!-- Title - only shows when needed, transparent background -->
<div class="title-main" id="titleMain">
MbetterClient Video Player
</div> </div>
<!-- Subtitle - only shows when needed, transparent background --> <div class="test-overlay">
<div class="title-subtitle" id="titleSubtitle"> OVERLAY TEST<br>
Ready for Content This should be VISIBLE<br>
If you see this, WebEngine is working
</div> </div>
<!-- News ticker - positioned at bottom -->
<div class="news-ticker" id="newsTicker">
<div class="ticker-text" id="tickerText">
Welcome to MbetterClient • Professional Video Overlay System • Real-time Updates • Hardware Accelerated Playback
</div>
</div>
<!-- Stats panel - positioned at side -->
<div class="stats-panel" id="statsPanel" style="display: none;">
<div class="stats-item">
<span>Resolution:</span>
<span id="resolution">1920x1080</span>
</div>
<div class="stats-item">
<span>Bitrate:</span>
<span id="bitrate">5.2 Mbps</span>
</div>
<div class="stats-item">
<span>Codec:</span>
<span id="codec">H.264</span>
</div>
<div class="stats-item">
<span>FPS:</span>
<span id="fps">30.0</span>
</div>
</div>
<!-- Canvas for custom animations - transparent background -->
<canvas class="canvas-overlay" id="canvasOverlay"></canvas>
<!-- Progress bar at bottom -->
<div class="progress-bar" id="progressBar" style="width: 0%;"></div>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script> <script>
class OverlayManager { console.log('=== WebEngine Overlay Test v4.0 ===');
constructor() { console.log('Document loaded');
this.channel = null; console.log('Body background:', window.getComputedStyle(document.body).backgroundColor);
this.overlayData = {};
this.canvas = null; // Simple test - change background after 2 seconds
this.ctx = null; setTimeout(() => {
this.animationFrame = null; document.body.style.background = 'rgba(0, 0, 255, 0.3)'; // Blue
this.webChannelReady = false; console.log('Background changed to blue');
this.pendingUpdates = []; }, 2000);
// Wait for DOM to be fully loaded before accessing elements
this.waitForDOM(() => {
this.canvas = document.getElementById('canvasOverlay');
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
// Resize canvas to match window
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
// Start canvas animations
this.startCanvasAnimations();
}
// Initialize WebChannel after DOM is ready
this.initWebChannel();
});
console.log('OverlayManager constructor called');
}
waitForDOM(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
} else {
callback();
}
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
initWebChannel() {
try {
if (typeof qt === 'undefined' || !qt.webChannelTransport) {
console.log('WebChannel transport not ready, retrying in 200ms...');
setTimeout(() => this.initWebChannel(), 200);
return;
}
new QWebChannel(qt.webChannelTransport, (channel) => {
// Validate channel and critical objects exist
if (!channel || !channel.objects) {
console.warn('Invalid WebChannel received, retrying...');
setTimeout(() => this.initWebChannel(), 300);
return;
}
this.channel = channel;
// Wait for both DOM and WebChannel to be fully ready
this.waitForFullInitialization(() => {
this.webChannelReady = true;
// Register for updates from Python with error handling
if (channel.objects.overlay) {
try {
channel.objects.overlay.dataUpdated.connect((data) => {
this.updateOverlay(data);
});
channel.objects.overlay.positionChanged.connect((position, duration) => {
this.updateProgress(position, duration);
});
channel.objects.overlay.videoInfoChanged.connect((info) => {
this.updateVideoInfo(info);
});
console.log('WebChannel connected and ready');
// Process any pending updates after full initialization
setTimeout(() => this.processPendingUpdates(), 100);
} catch (connectError) {
console.error('Error connecting WebChannel signals:', connectError);
}
} else {
console.warn('WebChannel overlay object not found');
}
});
});
} catch (error) {
console.error('WebChannel initialization error:', error);
// Retry with exponential backoff
setTimeout(() => this.initWebChannel(), 1000);
}
}
waitForFullInitialization(callback) {
const checkReady = () => {
if (document.readyState === 'complete' && this.validateCriticalElements()) {
callback();
} else {
setTimeout(checkReady, 100);
}
};
checkReady();
}
processPendingUpdates() {
// Prevent infinite loops by limiting processing attempts
let processed = 0;
const maxProcessing = 10;
while (this.pendingUpdates.length > 0 && processed < maxProcessing) {
// Double-check readiness before processing
if (!this.isSystemReady()) {
console.log('System not ready during pending updates processing');
break;
}
const update = this.pendingUpdates.shift();
this.updateOverlay(update);
processed++;
}
// If there are still pending updates, schedule another processing cycle
if (this.pendingUpdates.length > 0) {
setTimeout(() => this.processPendingUpdates(), 300);
}
}
updateOverlay(data) {
console.log('Updating overlay with data:', data);
// Enhanced readiness check with multiple validation layers
if (!this.isSystemReady()) {
console.log('System not ready, queuing update');
this.pendingUpdates.push(data);
// Retry with progressive backoff
setTimeout(() => this.processPendingUpdates(), 150);
return;
}
// Validate all critical elements exist before any updates
if (!this.validateCriticalElements()) {
console.warn('Critical elements not available, requeueing update');
this.pendingUpdates.push(data);
setTimeout(() => this.processPendingUpdates(), 200);
return;
}
this.overlayData = { ...this.overlayData, ...data };
// Update title elements with safe element access
if (data.title !== undefined) {
if (!this.safeUpdateElement('titleMain', data.title, 'textContent')) {
console.warn('Failed to update titleMain, queuing for retry');
this.pendingUpdates.push({title: data.title});
return;
}
}
if (data.subtitle !== undefined) {
if (!this.safeUpdateElement('titleSubtitle', data.subtitle, 'textContent')) {
console.warn('Failed to update titleSubtitle, queuing for retry');
this.pendingUpdates.push({subtitle: data.subtitle});
return;
}
}
// Update ticker text
if (data.ticker !== undefined) {
if (!this.safeUpdateElement('tickerText', data.ticker, 'textContent')) {
console.warn('Failed to update tickerText, queuing for retry');
this.pendingUpdates.push({ticker: data.ticker});
return;
}
}
// Show/hide stats panel
if (data.showStats !== undefined) {
if (!this.safeUpdateElement('statsPanel', data.showStats ? 'block' : 'none', 'display')) {
console.warn('Failed to update statsPanel, queuing for retry');
this.pendingUpdates.push({showStats: data.showStats});
return;
}
}
// Update custom CSS if provided
if (data.customCSS) {
this.applyCustomCSS(data.customCSS);
}
}
isSystemReady() {
try {
return this.webChannelReady &&
document.readyState === 'complete' &&
document.getElementById('titleMain') !== null &&
document.body !== null;
} catch (error) {
console.warn('Error in isSystemReady check:', error);
return false;
}
}
validateCriticalElements() {
try {
const criticalIds = ['titleMain', 'titleSubtitle', 'tickerText', 'statsPanel', 'progressBar'];
for (const id of criticalIds) {
const element = document.getElementById(id);
if (!element) {
console.warn(`Critical element ${id} not found`);
return false;
}
// Additional check for element validity
if (element.parentNode === null || !document.contains(element)) {
console.warn(`Critical element ${id} not properly attached to DOM`);
return false;
}
}
return true;
} catch (error) {
console.warn('Error in validateCriticalElements:', error);
return false;
}
}
safeUpdateElement(elementId, value, property = 'textContent') {
try {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`Element ${elementId} not found`);
return false;
}
// Double-check element is still valid and in DOM
if (element.parentNode === null) {
console.warn(`Element ${elementId} no longer in DOM`);
return false;
}
// Additional check for element accessibility
if (!document.contains(element)) {
console.warn(`Element ${elementId} not contained in document`);
return false;
}
if (property === 'display') {
element.style.display = value;
} else if (property === 'width') {
element.style.width = value;
} else if (property === 'textContent') {
// Check if textContent property exists and is writable
if ('textContent' in element) {
element.textContent = value || '';
// Animate only if element update succeeded
this.animateElement(elementId, 'pulse');
} else {
console.warn(`Element ${elementId} does not support textContent`);
return false;
}
}
return true;
} catch (error) {
console.error(`Error updating element ${elementId}:`, error);
return false;
}
}
updateProgress(position, duration) {
try {
// Check system readiness before updating progress
if (!this.isSystemReady()) {
console.log('System not ready for progress update, skipping');
return;
}
const percentage = duration > 0 ? (position / duration) * 100 : 0;
// Safe progress bar update
this.safeUpdateElement('progressBar', `${percentage}%`, 'width');
} catch (error) {
console.error('Error updating progress:', error);
}
}
updateVideoInfo(info) {
console.log('Video info updated:', info);
if (info.resolution) {
const resolutionElement = document.getElementById('resolution');
if (resolutionElement) {
resolutionElement.textContent = info.resolution;
}
}
if (info.bitrate) {
const bitrateElement = document.getElementById('bitrate');
if (bitrateElement) {
bitrateElement.textContent = info.bitrate;
}
}
if (info.codec) {
const codecElement = document.getElementById('codec');
if (codecElement) {
codecElement.textContent = info.codec;
}
}
if (info.fps) {
const fpsElement = document.getElementById('fps');
if (fpsElement) {
fpsElement.textContent = info.fps;
}
}
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
animateElement(elementId, animationClass) {
const element = document.getElementById(elementId);
if (element) {
element.style.animation = 'none';
element.offsetHeight; // Trigger reflow
element.style.animation = `${animationClass} 1s ease-in-out`;
}
}
applyCustomCSS(css) {
let styleElement = document.getElementById('customStyles');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'customStyles';
document.head.appendChild(styleElement);
}
styleElement.textContent = css;
}
startCanvasAnimations() {
if (!this.canvas || !this.ctx) {
console.warn('Canvas not ready for animations');
return;
}
const animate = () => {
if (this.ctx && this.canvas) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw animated particles or custom graphics
this.drawParticles();
this.animationFrame = requestAnimationFrame(animate);
}
};
animate();
}
drawParticles() {
// Example particle system - can be customized
const time = Date.now() * 0.001;
for (let i = 0; i < 5; i++) {
const x = (Math.sin(time + i) * 100) + this.canvas.width / 2;
const y = (Math.cos(time + i * 0.5) * 50) + this.canvas.height / 2;
this.ctx.beginPath();
this.ctx.arc(x, y, 2, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(255, 255, 255, ${0.1 + Math.sin(time + i) * 0.1})`;
this.ctx.fill();
}
}
// Public API for Python to call
setTitle(title) {
this.updateOverlay({ title });
}
setSubtitle(subtitle) {
this.updateOverlay({ subtitle });
}
setTicker(ticker) {
this.updateOverlay({ ticker });
}
showStats(show) {
this.updateOverlay({ showStats: show });
}
cleanup() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
}
}
}
// Initialize overlay manager immediately and safely
let overlayManager = null;
// Function to ensure DOM is ready before any operations
function ensureOverlayReady(callback) {
if (overlayManager && overlayManager.webChannelReady) {
callback();
} else {
setTimeout(() => ensureOverlayReady(callback), 50);
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
overlayManager = new OverlayManager();
window.overlayManager = overlayManager;
});
} else {
// DOM already loaded
overlayManager = new OverlayManager();
window.overlayManager = overlayManager;
}
// Cleanup on unload
window.addEventListener('beforeunload', () => {
if (overlayManager) {
overlayManager.cleanup();
}
});
// Safe global functions that wait for overlay to be ready
window.safeUpdateOverlay = function(data) {
ensureOverlayReady(() => {
if (overlayManager) {
overlayManager.updateOverlay(data);
}
});
};
window.safeUpdateProgress = function(position, duration) {
ensureOverlayReady(() => {
if (overlayManager) {
overlayManager.updateProgress(position, duration);
}
});
};
</script> </script>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -153,28 +153,22 @@ class OverlayWebView(QWebEngineView): ...@@ -153,28 +153,22 @@ class OverlayWebView(QWebEngineView):
logger.info("OverlayWebView initialized") logger.info("OverlayWebView initialized")
def setup_web_view(self): def setup_web_view(self):
"""Setup web view as embedded overlay with pointer-event transparency""" """Setup web view with proper transparency for overlay"""
logger.debug("OverlayWebView.setup_web_view() - Starting embedded setup") logger.debug("OverlayWebView.setup_web_view() - Starting setup")
# Configure page settings for proper rendering with transparent background # Set transparent background on the web page
page = self.page() page = self.page()
logger.debug("Setting page background to transparent (0,0,0,0)")
page.setBackgroundColor(QColor(0, 0, 0, 0)) # Transparent page background page.setBackgroundColor(QColor(0, 0, 0, 0)) # Transparent page background
# CRITICAL: Use pointer-events approach instead of widget transparency to avoid desktop bleed # Widget should be visible but allow transparency
self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, False)
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
# DO NOT use WA_TranslucentBackground - it causes desktop transparency issues
# Style for overlay that allows video to show through background areas
self.setStyleSheet(""" self.setStyleSheet("""
QWebEngineView { QWebEngineView {
background: transparent;
border: none; border: none;
background: transparent;
} }
""") """)
logger.debug("OverlayWebView embedded setup completed") logger.debug("OverlayWebView setup completed with transparent page background")
# Setup WebChannel # Setup WebChannel
self.web_channel = QWebChannel() self.web_channel = QWebChannel()
...@@ -291,64 +285,78 @@ class NativeOverlayWidget(QWidget): ...@@ -291,64 +285,78 @@ class NativeOverlayWidget(QWidget):
logger.info("NativeOverlayWidget initialized") logger.info("NativeOverlayWidget initialized")
def setup_ui(self): def setup_ui(self):
"""Setup native Qt overlay widgets""" """Setup native Qt overlay widgets with forced visibility"""
# Make overlay transparent to mouse events but NOT to paint events logger.debug("NativeOverlayWidget.setup_ui() - Starting")
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
# Use transparent background but maintain proper widget layering # FORCE widget to be completely visible with bright background
self.setStyleSheet("background: rgba(0,0,0,0);") self.setStyleSheet("""
# Ensure overlay doesn't interfere with window opacity NativeOverlayWidget {
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, False) background-color: rgba(255, 0, 255, 200);
border: 5px solid cyan;
# Main title label }
""")
# Ensure widget is visible
self.setVisible(True)
self.setAutoFillBackground(True)
self.raise_() # Bring to front
# Main title label - ENHANCED VISIBILITY
self.title_label = QLabel() self.title_label = QLabel()
self.title_label.setStyleSheet(""" self.title_label.setStyleSheet("""
QLabel { QLabel {
color: white; color: white;
font-size: 32px; font-size: 32px;
font-weight: bold; font-weight: bold;
background: transparent; background: rgba(0,0,0,0.8);
padding: 10px; padding: 15px;
border-radius: 5px; border-radius: 8px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8); border: 2px solid rgba(255,255,255,0.3);
text-shadow: 2px 2px 4px rgba(0,0,0,1.0);
} }
""") """)
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title_label.setWordWrap(True) self.title_label.setWordWrap(True)
# Subtitle label # Subtitle label - ENHANCED VISIBILITY
self.subtitle_label = QLabel() self.subtitle_label = QLabel()
self.subtitle_label.setStyleSheet(""" self.subtitle_label.setStyleSheet("""
QLabel { QLabel {
color: #cccccc; color: #ffffff;
font-size: 18px; font-size: 18px;
background: transparent; background: rgba(0,0,0,0.7);
padding: 5px; padding: 10px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.8); border-radius: 5px;
text-shadow: 1px 1px 2px rgba(0,0,0,1.0);
} }
""") """)
self.subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Time display # Time display - ENHANCED VISIBILITY
self.time_label = QLabel() self.time_label = QLabel()
self.time_label.setStyleSheet(""" self.time_label.setStyleSheet("""
QLabel { QLabel {
color: white; color: yellow;
font-size: 16px; font-size: 16px;
background: rgba(0,0,0,0.5); font-weight: bold;
background: rgba(0,0,0,0.8);
padding: 8px; padding: 8px;
border-radius: 5px; border-radius: 5px;
border: 1px solid yellow;
} }
""") """)
# Ticker label # Ticker label - ENHANCED VISIBILITY
self.ticker_label = QLabel() self.ticker_label = QLabel()
self.ticker_label.setStyleSheet(""" self.ticker_label.setStyleSheet("""
QLabel { QLabel {
color: white; color: white;
font-size: 14px; font-size: 14px;
background: rgba(220, 53, 69, 0.9); font-weight: bold;
padding: 8px; background: rgba(220, 53, 69, 0.95);
padding: 12px;
border-radius: 5px; border-radius: 5px;
border: 2px solid rgba(255,255,255,0.3);
} }
""") """)
self.ticker_label.setWordWrap(True) self.ticker_label.setWordWrap(True)
...@@ -460,24 +468,31 @@ class VideoWidget(QWidget): ...@@ -460,24 +468,31 @@ class VideoWidget(QWidget):
logger.info(f"VideoWidget initialized (native_overlay={use_native_overlay})") logger.info(f"VideoWidget initialized (native_overlay={use_native_overlay})")
def setup_ui(self): def setup_ui(self):
"""Setup video player with selectable overlay type""" """Setup video player with absolute positioning - NO LAYOUTS"""
# CRITICAL: Container must be completely opaque to prevent desktop bleed-through logger.debug("VideoWidget.setup_ui() - Starting setup with absolute positioning")
self.setStyleSheet("VideoWidget { background-color: black; }")
self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, True)
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, False)
# Create layout for the base video widget # NO LAYOUT - use absolute positioning for both video and overlay
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) # LAYER 1: Video widget with absolute positioning
layout.setSpacing(0) self.video_widget = QVideoWidget(self)
# LAYER 1: Video widget as base layer - this shows the actual video
self.video_widget = QVideoWidget()
self.video_widget.setStyleSheet("QVideoWidget { background-color: black; }") self.video_widget.setStyleSheet("QVideoWidget { background-color: black; }")
self.video_widget.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, True) self.video_widget.setGeometry(0, 0, 800, 600)
layout.addWidget(self.video_widget) self.video_widget.show()
# SIMPLE TEST: Add a basic QFrame as overlay with absolute positioning
self.test_frame = QFrame(self)
self.test_frame.setStyleSheet("""
QFrame {
background-color: red;
border: 5px solid yellow;
}
""")
self.test_frame.setGeometry(50, 50, 200, 100)
self.test_frame.setAutoFillBackground(True)
self.test_frame.raise_()
self.test_frame.show()
# LAYER 2: Overlay as child widget positioned on top (NOT in layout) # LAYER 2: Original overlay widget with absolute positioning
if self.use_native_overlay: if self.use_native_overlay:
self.overlay_view = NativeOverlayWidget(self) self.overlay_view = NativeOverlayWidget(self)
logger.debug("VideoWidget using native Qt overlay") logger.debug("VideoWidget using native Qt overlay")
...@@ -485,25 +500,32 @@ class VideoWidget(QWidget): ...@@ -485,25 +500,32 @@ class VideoWidget(QWidget):
self.overlay_view = OverlayWebView(self) self.overlay_view = OverlayWebView(self)
logger.debug("VideoWidget using QWebEngineView overlay") logger.debug("VideoWidget using QWebEngineView overlay")
# CRITICAL: Don't add overlay to layout - position it manually as floating child # Position overlay with absolute positioning
# This allows the video widget to render underneath self.overlay_view.setGeometry(300, 50, 400, 300)
self.overlay_view.setParent(self) self.overlay_view.raise_()
self.overlay_view.raise_() # Ensure overlay is on top self.overlay_view.show()
logger.debug(f"VideoWidget overlay setup completed (native={self.use_native_overlay})") logger.debug(f"VideoWidget overlay setup completed (native={self.use_native_overlay})")
logger.debug(f"Video widget geometry: {self.video_widget.geometry()}")
logger.debug(f"Test frame geometry: {self.test_frame.geometry()}")
logger.debug(f"Overlay geometry: {self.overlay_view.geometry()}")
def resizeEvent(self, event): def resizeEvent(self, event):
"""Handle resize events""" """Handle resize events"""
super().resizeEvent(event) super().resizeEvent(event)
# Position overlay to cover the video widget area self._position_overlay()
if self.overlay_view:
# Position overlay to match the video widget exactly def _position_overlay(self):
video_geometry = self.video_widget.geometry() """Position overlays with absolute positioning"""
self.overlay_view.setGeometry(video_geometry) if hasattr(self, 'test_frame') and self.test_frame:
self.test_frame.raise_()
self.test_frame.show()
# Position overlay to exactly cover video widget if hasattr(self, 'overlay_view') and self.overlay_view:
self.overlay_view.raise_() # Ensure overlay stays on top self.overlay_view.raise_()
logger.debug(f"Overlay repositioned: {video_geometry}") self.overlay_view.show()
logger.debug(f"Widgets repositioned - size: {self.width()}x{self.height()}")
def get_video_widget(self) -> QVideoWidget: def get_video_widget(self) -> QVideoWidget:
"""Get the video widget for media player""" """Get the video widget for media player"""
...@@ -693,11 +715,36 @@ class PlayerWindow(QMainWindow): ...@@ -693,11 +715,36 @@ class PlayerWindow(QMainWindow):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0) layout.setSpacing(0)
# Video widget with overlay - NO CONTROLS # SIMPLE VIDEO WIDGET ONLY - overlay as separate top-level window
use_native = getattr(self.settings, 'use_native_overlay', False) use_native = getattr(self.settings, 'use_native_overlay', False)
logger.debug(f"PlayerWindow: use_native_overlay setting = {use_native}") logger.debug(f"PlayerWindow: use_native_overlay setting = {use_native}")
self.video_widget = VideoWidget(parent=self, use_native_overlay=use_native)
layout.addWidget(self.video_widget, 1) # Full stretch - no controls # Create simple video widget without any overlay
self.video_widget = VideoWidget(parent=central_widget, use_native_overlay=False)
layout.addWidget(self.video_widget, 1)
# Create overlay as SEPARATE TOP-LEVEL WINDOW
self.overlay_window = QWidget()
self.overlay_window.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool
)
self.overlay_window.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.overlay_window.setStyleSheet("background: transparent;")
# Create overlay widget inside the separate window
if use_native:
self.window_overlay = NativeOverlayWidget(self.overlay_window)
logger.debug("PlayerWindow: Created native overlay as separate window")
else:
self.window_overlay = OverlayWebView(self.overlay_window)
logger.debug("PlayerWindow: Created WebEngine overlay as separate window")
# Layout for overlay window
overlay_layout = QVBoxLayout(self.overlay_window)
overlay_layout.setContentsMargins(0, 0, 0, 0)
overlay_layout.addWidget(self.window_overlay)
# Controls removed per user request - clean overlay-only interface # Controls removed per user request - clean overlay-only interface
self.controls = None self.controls = None
...@@ -722,9 +769,43 @@ class PlayerWindow(QMainWindow): ...@@ -722,9 +769,43 @@ class PlayerWindow(QMainWindow):
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint) self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
self.show() # Reshow after flag change self.show() # Reshow after flag change
# Position overlay window AFTER main window is shown and positioned
QTimer.singleShot(100, self._sync_overlay_position)
# Setup menu # Setup menu
self.setup_menu() self.setup_menu()
def _sync_overlay_position(self):
"""Synchronize overlay window position with main player window"""
try:
if hasattr(self, 'overlay_window') and self.overlay_window:
# Get main window geometry
main_geo = self.geometry()
logger.debug(f"Main window geometry: {main_geo}")
# Position overlay window exactly over main window
self.overlay_window.setGeometry(main_geo)
self.overlay_window.show()
self.overlay_window.raise_()
logger.debug(f"Overlay window positioned at: {self.overlay_window.geometry()}")
except Exception as e:
logger.error(f"Failed to sync overlay position: {e}")
def resizeEvent(self, event):
"""Handle window resize events - sync overlay position"""
super().resizeEvent(event)
if hasattr(self, 'overlay_window') and self.overlay_window:
# Delay sync to allow window to finish resizing
QTimer.singleShot(10, self._sync_overlay_position)
def moveEvent(self, event):
"""Handle window move events - sync overlay position"""
super().moveEvent(event)
if hasattr(self, 'overlay_window') and self.overlay_window:
# Delay sync to allow window to finish moving
QTimer.singleShot(10, self._sync_overlay_position)
def setup_menu(self): def setup_menu(self):
"""Setup application menu""" """Setup application menu"""
...@@ -756,7 +837,12 @@ class PlayerWindow(QMainWindow): ...@@ -756,7 +837,12 @@ class PlayerWindow(QMainWindow):
self.audio_output = QAudioOutput() self.audio_output = QAudioOutput()
self.media_player.setAudioOutput(self.audio_output) self.media_player.setAudioOutput(self.audio_output)
self.media_player.setVideoOutput(self.video_widget.get_video_widget()) # For VideoWidget without internal overlay, get the base video widget directly
if hasattr(self.video_widget, 'get_video_widget'):
self.media_player.setVideoOutput(self.video_widget.get_video_widget())
else:
# Simple VideoWidget case
self.media_player.setVideoOutput(self.video_widget.video_widget)
# Connect signals # Connect signals
self.media_player.playbackStateChanged.connect(self.on_state_changed) self.media_player.playbackStateChanged.connect(self.on_state_changed)
...@@ -796,38 +882,51 @@ class PlayerWindow(QMainWindow): ...@@ -796,38 +882,51 @@ class PlayerWindow(QMainWindow):
def play_video(self, file_path: str, template_data: Dict[str, Any] = None): def play_video(self, file_path: str, template_data: Dict[str, Any] = None):
"""Play video file with optional overlay data""" """Play video file with optional overlay data"""
try: try:
logger.info(f"PlayerWindow.play_video() called with: {file_path}")
with QMutexLocker(self.mutex): with QMutexLocker(self.mutex):
# Handle both absolute and relative file paths # Handle both absolute and relative file paths
path_obj = Path(file_path) path_obj = Path(file_path)
logger.info(f"Original path object: {path_obj}")
if not path_obj.is_absolute(): if not path_obj.is_absolute():
# For relative paths, resolve them relative to the application directory # For relative paths, resolve them relative to the application directory
import os import os
app_dir = Path(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) app_dir = Path(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
absolute_path = app_dir / file_path absolute_path = app_dir / file_path
logger.debug(f"Resolving relative path '{file_path}' to '{absolute_path}'") logger.info(f"Resolved relative path '{file_path}' to '{absolute_path}'")
else: else:
absolute_path = path_obj absolute_path = path_obj
logger.info(f"Using absolute path: {absolute_path}")
# Verify file exists before trying to play # Verify file exists before trying to play
logger.info(f"Checking if file exists: {absolute_path}")
if not absolute_path.exists(): if not absolute_path.exists():
logger.error(f"Video file not found: {absolute_path}") logger.error(f"Video file not found: {absolute_path}")
# Show error in overlay # Show error in overlay
overlay_view = self.video_widget.get_overlay_view() if hasattr(self, 'window_overlay'):
error_data = { overlay_view = self.window_overlay
'title': 'File Not Found', error_data = {
'subtitle': f'Cannot find: {file_path}', 'title': 'File Not Found',
'ticker': 'Please check the file path and try again.' 'subtitle': f'Cannot find: {file_path}',
} 'ticker': 'Please check the file path and try again.'
if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel: }
if self._is_webengine_ready(overlay_view): if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
if self._is_webengine_ready(overlay_view):
overlay_view.update_overlay_data(error_data)
else:
overlay_view.update_overlay_data(error_data) overlay_view.update_overlay_data(error_data)
else:
overlay_view.update_overlay_data(error_data)
return return
logger.info(f"File exists! Size: {absolute_path.stat().st_size} bytes")
url = QUrl.fromLocalFile(str(absolute_path)) url = QUrl.fromLocalFile(str(absolute_path))
logger.info(f"Loading video URL: {url.toString()}") logger.info(f"Created QUrl: {url.toString()}")
logger.info(f"QUrl is valid: {url.isValid()}")
logger.info(f"Media player current state: {self.media_player.playbackState()}")
self.media_player.setSource(url) self.media_player.setSource(url)
logger.info(f"Media player source set to: {url.toString()}")
# Update overlay with video info using safe method # Update overlay with video info using safe method
overlay_data = template_data or {} overlay_data = template_data or {}
...@@ -836,13 +935,14 @@ class PlayerWindow(QMainWindow): ...@@ -836,13 +935,14 @@ class PlayerWindow(QMainWindow):
'subtitle': 'MbetterClient PyQt6 Player' 'subtitle': 'MbetterClient PyQt6 Player'
}) })
overlay_view = self.video_widget.get_overlay_view() if hasattr(self, 'window_overlay'):
if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel: overlay_view = self.window_overlay
# QWebEngineView overlay - use safe update if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
QTimer.singleShot(1000, lambda: self._send_safe_overlay_update(overlay_view, overlay_data)) # QWebEngineView overlay - use safe update
else: QTimer.singleShot(1000, lambda: self._send_safe_overlay_update(overlay_view, overlay_data))
# Native Qt overlay - immediate update is safe else:
overlay_view.update_overlay_data(overlay_data) # Native Qt overlay - immediate update is safe
overlay_view.update_overlay_data(overlay_data)
if self.settings.auto_play: if self.settings.auto_play:
self.media_player.play() self.media_player.play()
...@@ -863,8 +963,8 @@ class PlayerWindow(QMainWindow): ...@@ -863,8 +963,8 @@ class PlayerWindow(QMainWindow):
def on_metadata_extracted(self, metadata: Dict[str, Any]): def on_metadata_extracted(self, metadata: Dict[str, Any]):
"""Handle extracted metadata""" """Handle extracted metadata"""
if 'error' not in metadata: if 'error' not in metadata and hasattr(self, 'window_overlay'):
self.video_widget.get_overlay_view().update_video_info(metadata) self.window_overlay.update_video_info(metadata)
logger.debug(f"Video metadata: {metadata}") logger.debug(f"Video metadata: {metadata}")
def toggle_play_pause(self): def toggle_play_pause(self):
...@@ -902,9 +1002,9 @@ class PlayerWindow(QMainWindow): ...@@ -902,9 +1002,9 @@ class PlayerWindow(QMainWindow):
def toggle_stats(self): def toggle_stats(self):
"""Toggle stats panel""" """Toggle stats panel"""
overlay_view = self.video_widget.get_overlay_view() overlay_view = self.window_overlay
# Toggle stats via WebChannel # Toggle stats via WebChannel
if overlay_view.overlay_channel: if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
current_stats = getattr(self, '_stats_visible', False) current_stats = getattr(self, '_stats_visible', False)
overlay_view.overlay_channel.toggleStats(not current_stats) overlay_view.overlay_channel.toggleStats(not current_stats)
self._stats_visible = not current_stats self._stats_visible = not current_stats
...@@ -921,8 +1021,8 @@ class PlayerWindow(QMainWindow): ...@@ -921,8 +1021,8 @@ class PlayerWindow(QMainWindow):
# Controls removed - no slider updates needed # Controls removed - no slider updates needed
# Update overlay with position info using safe method # Update overlay with position info using safe method
if duration > 0: if duration > 0 and hasattr(self, 'window_overlay'):
overlay_view = self.video_widget.get_overlay_view() overlay_view = self.window_overlay
if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel: if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
# QWebEngineView overlay - check readiness before position update # QWebEngineView overlay - check readiness before position update
...@@ -952,21 +1052,22 @@ class PlayerWindow(QMainWindow): ...@@ -952,21 +1052,22 @@ class PlayerWindow(QMainWindow):
logger.error(f"Media player error: {error}") logger.error(f"Media player error: {error}")
# Show error in overlay using safe update # Show error in overlay using safe update
overlay_view = self.video_widget.get_overlay_view() if hasattr(self, 'window_overlay'):
error_data = { overlay_view = self.window_overlay
'title': 'Playback Error', error_data = {
'subtitle': f'Error: {error.name if hasattr(error, "name") else str(error)}', 'title': 'Playback Error',
'ticker': 'Please check the video file and try again.' 'subtitle': f'Error: {error.name if hasattr(error, "name") else str(error)}',
} 'ticker': 'Please check the video file and try again.'
}
if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
# QWebEngineView overlay - use safe update if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
if self._is_webengine_ready(overlay_view): # QWebEngineView overlay - use safe update
if self._is_webengine_ready(overlay_view):
overlay_view.update_overlay_data(error_data)
# Skip if not ready
else:
# Native Qt overlay - always safe
overlay_view.update_overlay_data(error_data) overlay_view.update_overlay_data(error_data)
# Skip if not ready
else:
# Native Qt overlay - always safe
overlay_view.update_overlay_data(error_data)
def on_media_status_changed(self, status): def on_media_status_changed(self, status):
"""Handle media status changes""" """Handle media status changes"""
...@@ -974,33 +1075,35 @@ class PlayerWindow(QMainWindow): ...@@ -974,33 +1075,35 @@ class PlayerWindow(QMainWindow):
if status == QMediaPlayer.MediaStatus.LoadedMedia: if status == QMediaPlayer.MediaStatus.LoadedMedia:
# Media loaded successfully - use safe update # Media loaded successfully - use safe update
overlay_view = self.video_widget.get_overlay_view() if hasattr(self, 'window_overlay'):
status_data = {'subtitle': 'Media loaded successfully'} overlay_view = self.window_overlay
status_data = {'subtitle': 'Media loaded successfully'}
if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
# QWebEngineView overlay - use safe update if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
if self._is_webengine_ready(overlay_view): # QWebEngineView overlay - use safe update
if self._is_webengine_ready(overlay_view):
overlay_view.update_overlay_data(status_data)
# Skip if not ready
else:
# Native Qt overlay - always safe
overlay_view.update_overlay_data(status_data) overlay_view.update_overlay_data(status_data)
# Skip if not ready
else:
# Native Qt overlay - always safe
overlay_view.update_overlay_data(status_data)
def update_overlay_periodically(self): def update_overlay_periodically(self):
"""Periodic overlay updates with WebEngine safety checks""" """Periodic overlay updates with WebEngine safety checks"""
try: try:
current_time = time.strftime("%H:%M:%S") current_time = time.strftime("%H:%M:%S")
overlay_view = self.video_widget.get_overlay_view() if hasattr(self, 'window_overlay'):
overlay_view = self.window_overlay
# Use safe update for WebEngine overlays
if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel: # Use safe update for WebEngine overlays
# QWebEngineView overlay - check readiness if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
if self._is_webengine_ready(overlay_view): # QWebEngineView overlay - check readiness
if self._is_webengine_ready(overlay_view):
overlay_view.update_overlay_data({'currentTime': current_time})
# Skip update if not ready to prevent JavaScript errors
else:
# Native Qt overlay - always safe
overlay_view.update_overlay_data({'currentTime': current_time}) overlay_view.update_overlay_data({'currentTime': current_time})
# Skip update if not ready to prevent JavaScript errors
else:
# Native Qt overlay - always safe
overlay_view.update_overlay_data({'currentTime': current_time})
except Exception as e: except Exception as e:
logger.error(f"Periodic overlay update failed: {e}") logger.error(f"Periodic overlay update failed: {e}")
...@@ -1054,6 +1157,11 @@ class PlayerWindow(QMainWindow): ...@@ -1054,6 +1157,11 @@ class PlayerWindow(QMainWindow):
self.media_player.stop() self.media_player.stop()
self.thread_pool.waitForDone(3000) # Wait up to 3 seconds for threads self.thread_pool.waitForDone(3000) # Wait up to 3 seconds for threads
# Close overlay window
if hasattr(self, 'overlay_window') and self.overlay_window:
self.overlay_window.close()
logger.debug("Overlay window closed")
logger.info("Player window closing - Qt will handle application exit") logger.info("Player window closing - Qt will handle application exit")
event.accept() event.accept()
...@@ -1535,18 +1643,29 @@ class QtVideoPlayer: ...@@ -1535,18 +1643,29 @@ class QtVideoPlayer:
file_path = message.data.get("file_path") file_path = message.data.get("file_path")
template_data = message.data.get("overlay_data", {}) template_data = message.data.get("overlay_data", {})
logger.info(f"VIDEO_PLAY message received from {message.sender}")
logger.info(f"Message data: {message.data}")
if not file_path: if not file_path:
logger.error("No file path provided for video play") logger.error("No file path provided for video play")
return return
logger.info(f"Playing video: {file_path}") logger.info(f"Attempting to play video: {file_path}")
logger.info(f"Template data: {template_data}")
if self.window: if not self.window:
# Use Qt's signal/slot mechanism to ensure GUI operations happen on main thread logger.error("Qt player window not available")
self.window.play_video(file_path, template_data) return
# Use Qt's signal/slot mechanism to ensure GUI operations happen on main thread
logger.info("Calling window.play_video() method")
self.window.play_video(file_path, template_data)
logger.info("window.play_video() method completed")
except Exception as e: except Exception as e:
logger.error(f"Failed to handle video play: {e}") logger.error(f"Failed to handle video play: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
def _handle_video_pause(self, message: Message): def _handle_video_pause(self, message: Message):
"""Handle video pause message""" """Handle video pause message"""
...@@ -1607,9 +1726,9 @@ class QtVideoPlayer: ...@@ -1607,9 +1726,9 @@ class QtVideoPlayer:
try: try:
template_data = message.data.get("template_data", {}) template_data = message.data.get("template_data", {})
if self.window and template_data: if self.window and template_data and hasattr(self.window, 'window_overlay'):
# Use Qt's signal/slot mechanism to ensure GUI operations happen on main thread # Use Qt's signal/slot mechanism to ensure GUI operations happen on main thread
overlay_view = self.window.video_widget.get_overlay_view() overlay_view = self.window.window_overlay
overlay_view.update_overlay_data(template_data) overlay_view.update_overlay_data(template_data)
except Exception as e: except Exception as e:
...@@ -1620,9 +1739,9 @@ class QtVideoPlayer: ...@@ -1620,9 +1739,9 @@ class QtVideoPlayer:
try: try:
overlay_data = message.data.get("overlay_data", {}) overlay_data = message.data.get("overlay_data", {})
if self.window and overlay_data: if self.window and overlay_data and hasattr(self.window, 'window_overlay'):
# Use Qt's signal/slot mechanism to ensure GUI operations happen on main thread # Use Qt's signal/slot mechanism to ensure GUI operations happen on main thread
overlay_view = self.window.video_widget.get_overlay_view() overlay_view = self.window.window_overlay
overlay_view.update_overlay_data(overlay_data) overlay_view.update_overlay_data(overlay_data)
except Exception as e: except Exception as e:
......
#!/usr/bin/env python3
from PyQt6.QtWidgets import QApplication, QMainWindow
import sys
app = QApplication(sys.argv)
window = QMainWindow()
window.show()
sys.exit(app.exec())
#!/usr/bin/env python3
"""
Standalone test application for PyQt6 Video Player with QWebEngineView overlay
"""
import sys
import logging
import time
from pathlib import Path
from dataclasses import dataclass
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QTimer
# Add project path for imports
project_path = Path(__file__).parent
sys.path.insert(0, str(project_path))
from mbetterclient.qt_player.qt6_player import Qt6VideoPlayer, PlayerWindow
from mbetterclient.core.message_bus import MessageBus, MessageBuilder
from mbetterclient.config.settings import QtConfig
@dataclass
class TestQtConfig:
"""Test configuration for Qt player"""
fullscreen: bool = False
window_width: int = 1280
window_height: int = 720
always_on_top: bool = False
auto_play: bool = True
volume: float = 0.8
mute: bool = False
def setup_logging():
"""Setup logging for the test application"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('qt6_player_test.log')
]
)
def test_standalone_player():
"""Test the standalone PyQt6 player window"""
print("Testing Standalone PyQt6 Player...")
app = QApplication(sys.argv)
config = TestQtConfig()
# Create player window directly
window = PlayerWindow(config)
# Show window
window.show()
# Test overlay updates
overlay_view = window.video_widget.get_overlay_view()
def update_overlay_demo():
"""Demo function to update overlay periodically"""
current_time = time.strftime("%H:%M:%S")
overlay_data = {
'title': f'PyQt6 Demo - {current_time}',
'subtitle': 'Multi-threaded Video Player with WebEngine Overlay',
'ticker': 'Real-time JavaScript ↔ Python Communication • Hardware Accelerated Video • Professional Animations'
}
overlay_view.update_overlay_data(overlay_data)
print(f"Updated overlay at {current_time}")
# Setup periodic overlay updates
timer = QTimer()
timer.timeout.connect(update_overlay_demo)
timer.start(2000) # Update every 2 seconds
# Initial overlay update
update_overlay_demo()
print("PyQt6 Player Window created successfully!")
print("Features demonstrated:")
print("- QMediaPlayer + QVideoWidget for hardware-accelerated video")
print("- QWebEngineView overlay with transparent background")
print("- QWebChannel bidirectional Python ↔ JavaScript communication")
print("- CSS3 animations with GSAP integration")
print("- Thread-safe signal/slot mechanisms")
print("- QTimer integration for real-time updates")
print("- Professional UI with responsive design")
print("- Cross-platform compatibility")
print("\nControls:")
print("- Space: Play/Pause")
print("- F11: Toggle Fullscreen")
print("- S: Toggle Stats Panel")
print("- M: Toggle Mute")
print("- Escape: Exit")
print("\nClose the window to exit the test.")
return app.exec()
def test_threaded_player():
"""Test the full threaded PyQt6 player component"""
print("Testing Threaded PyQt6 Player Component...")
# Create message bus
message_bus = MessageBus()
# Create Qt config
config = TestQtConfig()
# Create Qt6 player component
player = Qt6VideoPlayer(message_bus, config)
# Initialize player
if not player.initialize():
print("Failed to initialize Qt6VideoPlayer!")
return 1
# Start player in separate thread (simulation)
print("Qt6VideoPlayer initialized successfully!")
# Test sending messages
def send_test_messages():
"""Send test messages to player"""
time.sleep(2)
# Test overlay update
overlay_message = MessageBuilder.template_change(
sender="test_app",
template_name="demo_template",
template_data={
'title': 'Threaded Player Demo',
'subtitle': 'Message Bus Communication Test',
'ticker': 'Successfully communicating via MessageBus • Multi-threaded Architecture • Real-time Updates'
}
)
overlay_message.recipient = "qt6_player"
message_bus.publish(overlay_message)
print("Sent overlay update message")
# Test video info update
time.sleep(2)
video_info_message = MessageBuilder.system_status(
sender="test_app",
status="demo",
details={
'videoInfo': {
'resolution': '1920x1080',
'bitrate': '8.5 Mbps',
'codec': 'H.265/HEVC',
'fps': '60.0'
}
}
)
video_info_message.recipient = "qt6_player"
message_bus.publish(video_info_message)
print("Sent video info update")
# Setup test message timer
timer = QTimer()
timer.timeout.connect(send_test_messages)
timer.setSingleShot(True)
timer.start(1000) # Start after 1 second
print("Threaded player test started. Close the player window to exit.")
# Run the player (this would normally be in a separate thread)
try:
player.run()
except KeyboardInterrupt:
print("Stopping player...")
player.shutdown()
return 0
def main():
"""Main test function"""
setup_logging()
print("PyQt6 Multi-threaded Video Player Test Suite")
print("=" * 50)
if len(sys.argv) > 1:
test_mode = sys.argv[1]
else:
print("Available test modes:")
print("1. standalone - Test standalone player window")
print("2. threaded - Test full threaded player component")
print()
test_mode = input("Select test mode (1 or 2): ").strip()
if test_mode == "1":
test_mode = "standalone"
elif test_mode == "2":
test_mode = "threaded"
else:
test_mode = "standalone"
try:
if test_mode == "standalone":
return test_standalone_player()
elif test_mode == "threaded":
return test_threaded_player()
else:
print(f"Unknown test mode: {test_mode}")
return 1
except Exception as e:
print(f"Test failed with error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script for Qt player functionality
"""
import sys
import os
import logging
import time
import threading
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from mbetterclient.config.settings import AppSettings
from mbetterclient.core.message_bus import MessageBus, MessageBuilder, MessageType
from mbetterclient.qt_player.player import QtVideoPlayer
def setup_logging():
"""Setup logging for the test"""
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def test_qt_player_standalone():
"""Test Qt player in standalone mode"""
logger = setup_logging()
logger.info("Starting Qt player standalone test")
# Create settings
settings = AppSettings()
settings.qt.fullscreen = False
settings.qt.window_width = 800
settings.qt.window_height = 600
# Create message bus
message_bus = MessageBus()
# Create Qt player
qt_player = QtVideoPlayer(message_bus, settings.qt)
# Initialize Qt player
if not qt_player.initialize():
logger.error("Failed to initialize Qt player")
return 1
logger.info("Qt player initialized successfully")
# Start message processing in a separate thread
qt_player.start_message_processing()
# Send a test message to display default overlay
test_message = MessageBuilder.template_change(
sender="test",
template_data={
"title": "Qt Player Test",
"subtitle": "Standalone Mode Test",
"ticker": "This is a test of the Qt player in standalone mode"
}
)
message_bus.publish(test_message)
# Run Qt event loop (this will block until window is closed)
logger.info("Running Qt event loop - close the window to exit")
exit_code = qt_player.run()
# Cleanup
qt_player.shutdown()
logger.info("Qt player test completed")
return exit_code
def test_qt_player_with_message_bus():
"""Test Qt player with message bus communication"""
logger = setup_logging()
logger.info("Starting Qt player message bus test")
# Create settings
settings = AppSettings()
settings.qt.fullscreen = False
settings.qt.window_width = 800
settings.qt.window_height = 600
# Create message bus
message_bus = MessageBus()
# Create Qt player
qt_player = QtVideoPlayer(message_bus, settings.qt)
# Initialize Qt player
if not qt_player.initialize():
logger.error("Failed to initialize Qt player")
return 1
logger.info("Qt player initialized successfully")
# Start message processing in a separate thread
qt_player.start_message_processing()
# Send test messages
def send_test_messages():
time.sleep(2) # Wait for window to be ready
# Send overlay update
overlay_message = MessageBuilder.overlay_update(
sender="test",
overlay_data={
"title": "Message Bus Test",
"subtitle": "Testing message bus communication",
"showStats": True
}
)
message_bus.publish(overlay_message)
time.sleep(3)
# Send another overlay update
overlay_message2 = MessageBuilder.overlay_update(
sender="test",
overlay_data={
"title": "Message Bus Test Continued",
"subtitle": "Second message bus test",
"ticker": "Testing continuous updates through message bus"
}
)
message_bus.publish(overlay_message2)
logger.info("Test messages sent")
# Start message sending in a separate thread
message_thread = threading.Thread(target=send_test_messages)
message_thread.start()
# Run Qt event loop (this will block until window is closed)
logger.info("Running Qt event loop with message bus test - close the window to exit")
exit_code = qt_player.run()
# Wait for message thread to finish
message_thread.join()
# Cleanup
qt_player.shutdown()
logger.info("Qt player message bus test completed")
return exit_code
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "standalone":
exit_code = test_qt_player_standalone()
elif len(sys.argv) > 1 and sys.argv[1] == "message_bus":
exit_code = test_qt_player_with_message_bus()
else:
print("Usage: python test_qt_player.py [standalone|message_bus]")
print(" standalone: Test Qt player in standalone mode")
print(" message_bus: Test Qt player with message bus communication")
sys.exit(1)
sys.exit(exit_code)
\ No newline at end of file
File added
#!/usr/bin/env python3
"""
Debug script specifically for testing video playback visibility in Qt player
Tests both native and WebEngine overlays to isolate the video blocking issue
"""
import sys
import logging
import time
from pathlib import Path
from dataclasses import dataclass
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QTimer
# Add project path for imports
project_path = Path(__file__).parent
sys.path.insert(0, str(project_path))
from mbetterclient.qt_player.player import PlayerWindow
@dataclass
class DebugQtConfig:
"""Debug configuration for Qt player"""
fullscreen: bool = False
window_width: int = 800
window_height: int = 600
always_on_top: bool = False
auto_play: bool = True
volume: float = 0.8
mute: bool = False
use_native_overlay: bool = True # Start with native overlay for testing
def setup_debug_logging():
"""Setup debug logging"""
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('video_debug.log')
]
)
def test_video_playback_native():
"""Test video playback with native overlay (should not block video)"""
print("Testing Video Playback with NATIVE overlay...")
app = QApplication(sys.argv)
config = DebugQtConfig()
config.use_native_overlay = True # Force native overlay
# Create player window with native overlay
window = PlayerWindow(config)
window.show()
# Test with our generated test video
test_video_path = "test_video.mp4"
def play_test_video():
"""Play the test video after a short delay"""
print(f"Playing test video: {test_video_path}")
window.play_video(test_video_path)
# Update overlay to confirm it's working
overlay_data = {
'title': 'DEBUG: Native Overlay Test',
'subtitle': 'Video should be VISIBLE underneath this overlay',
'ticker': 'If you can see moving colors/patterns, video is working! Native overlay should not block video.'
}
overlay_view = window.video_widget.get_overlay_view()
overlay_view.update_overlay_data(overlay_data)
# Play video after 2 seconds
QTimer.singleShot(2000, play_test_video)
print("Native Overlay Test Window created!")
print("Expected behavior:")
print("- You should see a test pattern video (moving colors/gradients)")
print("- Native overlay text should appear ON TOP of the video")
print("- If video is NOT visible, the issue is deeper than overlay blocking")
print("\nControls:")
print("- Space: Play/Pause")
print("- Escape: Exit")
return app.exec()
def test_video_playback_webengine():
"""Test video playback with WebEngine overlay (may block video)"""
print("Testing Video Playback with WEBENGINE overlay...")
app = QApplication(sys.argv)
config = DebugQtConfig()
config.use_native_overlay = False # Force WebEngine overlay
# Create player window with WebEngine overlay
window = PlayerWindow(config)
window.show()
# Test with our generated test video
test_video_path = "test_video.mp4"
def play_test_video():
"""Play the test video after a short delay"""
print(f"Playing test video: {test_video_path}")
window.play_video(test_video_path)
# Update overlay to confirm it's working
overlay_data = {
'title': 'DEBUG: WebEngine Overlay Test',
'subtitle': 'Video may be BLOCKED by this overlay',
'ticker': 'If you CANNOT see moving colors/patterns, WebEngine overlay is blocking the video!'
}
# Wait for WebEngine to be ready before updating
def update_overlay_when_ready():
overlay_view = window.video_widget.get_overlay_view()
if hasattr(overlay_view, 'overlay_channel') and overlay_view.overlay_channel:
if window._is_webengine_ready(overlay_view):
overlay_view.update_overlay_data(overlay_data)
print("WebEngine overlay updated")
else:
print("WebEngine not ready, retrying...")
QTimer.singleShot(1000, update_overlay_when_ready)
else:
overlay_view.update_overlay_data(overlay_data)
QTimer.singleShot(3000, update_overlay_when_ready)
# Play video after 2 seconds
QTimer.singleShot(2000, play_test_video)
print("WebEngine Overlay Test Window created!")
print("Expected behavior:")
print("- You should see a test pattern video (moving colors/gradients)")
print("- WebEngine overlay text should appear ON TOP of the video")
print("- If video is NOT visible, WebEngine overlay is blocking it")
print("\nControls:")
print("- Space: Play/Pause")
print("- Escape: Exit")
return app.exec()
def test_uploaded_video():
"""Test with an actual uploaded video file"""
print("Testing with uploaded video files...")
# Look for uploaded videos
uploads_dir = Path("uploads")
if uploads_dir.exists():
video_files = list(uploads_dir.glob("*.mp4"))
if video_files:
video_path = video_files[0] # Use first video found
print(f"Found uploaded video: {video_path}")
app = QApplication(sys.argv)
config = DebugQtConfig()
config.use_native_overlay = True # Start with native
window = PlayerWindow(config)
window.show()
def play_uploaded_video():
print(f"Playing uploaded video: {video_path}")
window.play_video(str(video_path))
overlay_data = {
'title': f'Playing: {video_path.name}',
'subtitle': 'Testing uploaded video with native overlay',
'ticker': 'This is a real uploaded video file. Video should be visible with native overlay.'
}
overlay_view = window.video_widget.get_overlay_view()
overlay_view.update_overlay_data(overlay_data)
QTimer.singleShot(2000, play_uploaded_video)
print(f"Testing uploaded video: {video_path.name}")
print("This tests with a real uploaded video file")
return app.exec()
else:
print("No video files found in uploads directory")
return 1
else:
print("Uploads directory not found")
return 1
def main():
"""Main debug function"""
setup_debug_logging()
print("Qt Video Player Debug Suite")
print("=" * 40)
if len(sys.argv) > 1:
test_mode = sys.argv[1]
else:
print("Available test modes:")
print("1. native - Test with native Qt overlay (should show video)")
print("2. webengine - Test with WebEngine overlay (may block video)")
print("3. uploaded - Test with uploaded video file")
print()
choice = input("Select test mode (1, 2, or 3): ").strip()
if choice == "1":
test_mode = "native"
elif choice == "2":
test_mode = "webengine"
elif choice == "3":
test_mode = "uploaded"
else:
test_mode = "native"
try:
if test_mode == "native":
return test_video_playback_native()
elif test_mode == "webengine":
return test_video_playback_webengine()
elif test_mode == "uploaded":
return test_uploaded_video()
else:
print(f"Unknown test mode: {test_mode}")
return 1
except Exception as e:
print(f"Test failed with error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())
\ No newline at end of file
#!/usr/bin/env python3
"""
Minimal video test - NO overlays at all to test pure QVideoWidget rendering
"""
import sys
import logging
from pathlib import Path
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from PyQt6.QtCore import QUrl
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtMultimediaWidgets import QVideoWidget
# Add project path for imports
project_path = Path(__file__).parent
sys.path.insert(0, str(project_path))
def setup_logging():
"""Setup basic logging"""
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stdout)]
)
class MinimalVideoWindow(QMainWindow):
"""Absolute minimal video player - NO overlays, just pure video"""
def __init__(self):
super().__init__()
self.setup_ui()
self.setup_media_player()
def setup_ui(self):
"""Setup minimal UI - just video widget"""
self.setWindowTitle("MINIMAL Video Test - NO Overlays")
self.setGeometry(100, 100, 800, 600)
# PURE BLACK BACKGROUND - no transparency anywhere
self.setStyleSheet("QMainWindow { background-color: black; }")
# Central widget - completely opaque
central_widget = QWidget()
central_widget.setStyleSheet("background-color: black;")
self.setCentralWidget(central_widget)
# Layout
layout = QVBoxLayout(central_widget)
layout.setContentsMargins(0, 0, 0, 0)
# ONLY QVideoWidget - no overlays at all
self.video_widget = QVideoWidget()
self.video_widget.setStyleSheet("QVideoWidget { background-color: black; }")
layout.addWidget(self.video_widget)
print("Minimal video window created - PURE QVideoWidget only")
def setup_media_player(self):
"""Setup media player"""
self.media_player = QMediaPlayer()
self.audio_output = QAudioOutput()
self.media_player.setAudioOutput(self.audio_output)
self.media_player.setVideoOutput(self.video_widget)
# Connect signals for debugging
self.media_player.playbackStateChanged.connect(self.on_state_changed)
self.media_player.mediaStatusChanged.connect(self.on_status_changed)
self.media_player.errorOccurred.connect(self.on_error)
print("Media player setup completed")
def play_video(self, file_path):
"""Play video file"""
path_obj = Path(file_path)
if not path_obj.exists():
print(f"ERROR: File not found: {file_path}")
return
print(f"Loading video: {file_path}")
print(f"File size: {path_obj.stat().st_size} bytes")
url = QUrl.fromLocalFile(str(path_obj.absolute()))
print(f"QUrl: {url.toString()}")
self.media_player.setSource(url)
self.media_player.play()
print("Video play command sent")
def on_state_changed(self, state):
"""Debug state changes"""
print(f"MEDIA STATE: {state}")
def on_status_changed(self, status):
"""Debug status changes"""
print(f"MEDIA STATUS: {status}")
def on_error(self, error):
"""Debug errors"""
print(f"MEDIA ERROR: {error}")
def keyPressEvent(self, event):
"""Handle keys"""
if event.key() == 32: # Space
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
self.media_player.pause()
print("PAUSED")
else:
self.media_player.play()
print("PLAYING")
def main():
"""Test minimal video rendering"""
setup_logging()
print("MINIMAL VIDEO TEST - NO OVERLAYS")
print("=" * 40)
print("This test uses ONLY QVideoWidget with NO overlays")
print("If video is not visible here, the issue is with QVideoWidget itself")
print("")
app = QApplication(sys.argv)
window = MinimalVideoWindow()
window.show()
# Play test video after delay
from PyQt6.QtCore import QTimer
QTimer.singleShot(1000, lambda: window.play_video("test_video.mp4"))
print("Window shown. Video should start playing in 1 second.")
print("Expected: You should see moving test pattern (countdown)")
print("Controls: Space = Play/Pause, Escape = Exit")
return app.exec()
if __name__ == "__main__":
sys.exit(main())
\ 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