Update player intro

parent b43952ee
......@@ -142,6 +142,24 @@ Examples:
action='store_true',
help='Enable debug mode showing only message bus messages'
)
parser.add_argument(
'--debug-messages',
action='store_true',
help='Show all messages passing through the message bus on screen'
)
parser.add_argument(
'--debug-player',
action='store_true',
help='Enable debug mode for Qt player component'
)
parser.add_argument(
'--debug-overlay',
action='store_true',
help='Enable debug mode for overlay rendering'
)
parser.add_argument(
'--no-qt',
......@@ -200,7 +218,7 @@ Examples:
'--start-timer',
type=int,
default=None,
help='Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received (default: 4 minutes)'
help='Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received. Use 0 for 10-second delay (default: 4 minutes)'
)
parser.add_argument(
......@@ -252,6 +270,9 @@ def main():
settings.web_port = args.web_port
settings.debug_mode = args.debug or args.dev_mode
settings.dev_message = args.dev_message
settings.debug_messages = args.debug_messages
settings.debug_player = args.debug_player
settings.debug_overlay = args.debug_overlay
settings.enable_qt = not args.no_qt
settings.enable_web = not args.no_web
settings.qt.use_native_overlay = args.native_overlay
......
......@@ -369,6 +369,9 @@ class AppSettings:
version: str = "1.0.0"
debug_mode: bool = False
dev_message: bool = False # Enable debug mode showing only message bus messages
debug_messages: bool = False # Show all messages passing through the message bus on screen
debug_player: bool = False # Enable debug mode for Qt player component
debug_overlay: bool = False # Enable debug mode for overlay rendering
enable_web: bool = True
enable_qt: bool = True
enable_api_client: bool = True
......@@ -408,6 +411,10 @@ class AppSettings:
"timer": self.timer.__dict__,
"version": self.version,
"debug_mode": self.debug_mode,
"dev_message": self.dev_message,
"debug_messages": self.debug_messages,
"debug_player": self.debug_player,
"debug_overlay": self.debug_overlay,
"enable_web": self.enable_web,
"enable_qt": self.enable_qt,
"enable_api_client": self.enable_api_client,
......@@ -438,7 +445,7 @@ class AppSettings:
settings.timer = TimerConfig(**data["timer"])
# Update app settings
for key in ["version", "debug_mode", "enable_web", "enable_qt", "enable_api_client", "enable_screen_cast"]:
for key in ["version", "debug_mode", "dev_message", "debug_messages", "debug_player", "debug_overlay", "enable_web", "enable_qt", "enable_api_client", "enable_screen_cast"]:
if key in data:
setattr(settings, key, data[key])
......
......@@ -165,7 +165,7 @@ class MbetterClientApplication:
def _initialize_message_bus(self) -> bool:
"""Initialize message bus"""
try:
self.message_bus = MessageBus(max_queue_size=1000, dev_message=self.settings.dev_message)
self.message_bus = MessageBus(max_queue_size=1000, dev_message=self.settings.dev_message, debug_messages=self.settings.debug_messages)
# Register core component
self.message_bus.register_component("core")
......@@ -322,7 +322,9 @@ class MbetterClientApplication:
self.qt_player = QtVideoPlayer(
message_bus=self.message_bus,
settings=self.settings.qt
settings=self.settings.qt,
debug_player=self.settings.debug_player,
debug_overlay=self.settings.debug_overlay
)
# Don't register with thread manager since QtPlayer no longer inherits from ThreadedComponent
......@@ -760,8 +762,13 @@ class MbetterClientApplication:
if self._start_timer_minutes is None:
return
delay_seconds = self._start_timer_minutes * 60
logger.info(f"Starting command line game timer: {self._start_timer_minutes} minutes ({delay_seconds} seconds)")
# Special case: --start-timer 0 means 10 seconds delay for system initialization
if self._start_timer_minutes == 0:
delay_seconds = 10
logger.info(f"Starting command line game timer: --start-timer 0 = 10 seconds delay for system initialization")
else:
delay_seconds = self._start_timer_minutes * 60
logger.info(f"Starting command line game timer: {self._start_timer_minutes} minutes ({delay_seconds} seconds)")
self._game_start_timer = threading.Timer(delay_seconds, self._on_game_timer_expired)
self._game_start_timer.daemon = True
......
......@@ -684,7 +684,11 @@ class GamesThread(ThreadedComponent):
# Start match timer
logger.info(f"⏰ Starting match timer for fixture {fixture_id}")
self._start_match_timer(fixture_id)
# Dispatch START_INTRO message
logger.info(f"🎬 Dispatching START_INTRO message for fixture {fixture_id}")
self._dispatch_start_intro(fixture_id)
# Refresh dashboard statuses
self._refresh_dashboard_statuses()
......@@ -897,6 +901,114 @@ class GamesThread(ThreadedComponent):
except Exception as e:
logger.error(f"Failed to start match timer: {e}")
def _dispatch_start_intro(self, fixture_id: str):
"""Dispatch START_INTRO message to trigger intro content"""
try:
from .message_bus import MessageBuilder
# Find the first match that was set to 'bet' status
first_bet_match_id = self._get_first_bet_match_id(fixture_id)
# Unzip the ZIP file of the first match if it exists
self._unzip_match_zip_file(first_bet_match_id)
# Create and send START_INTRO message
start_intro_message = MessageBuilder.start_intro(
sender=self.name,
fixture_id=fixture_id,
match_id=first_bet_match_id
)
self.message_bus.publish(start_intro_message, broadcast=True)
logger.info(f"START_INTRO message dispatched for fixture {fixture_id}, match {first_bet_match_id}")
except Exception as e:
logger.error(f"Failed to dispatch START_INTRO message: {e}")
def _get_first_bet_match_id(self, fixture_id: str) -> Optional[int]:
"""Get the ID of the first match set to 'bet' status in the fixture"""
try:
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel
# Find the first match with 'bet' status in this fixture
first_bet_match = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.status == 'bet',
MatchModel.active_status == True
).order_by(MatchModel.match_number.asc()).first()
if first_bet_match:
return first_bet_match.id
else:
logger.warning(f"No match with 'bet' status found in fixture {fixture_id}")
return None
finally:
session.close()
except Exception as e:
logger.error(f"Failed to get first bet match ID for fixture {fixture_id}: {e}")
return None
def _unzip_match_zip_file(self, match_id: int):
"""Unzip the ZIP file associated with a match to a temporary directory"""
try:
import zipfile
import tempfile
import os
from pathlib import Path
session = self.db_manager.get_session()
try:
# Get the match from database
match = session.query(MatchModel).filter_by(id=match_id).first()
if not match:
logger.warning(f"Match {match_id} not found, skipping ZIP extraction")
return
if not match.zip_filename:
logger.info(f"Match {match_id} has no associated ZIP file, skipping extraction")
return
# Determine ZIP file location (ZIP files are stored in the zip_files directory)
from ..config.settings import get_user_data_dir
user_data_dir = get_user_data_dir()
zip_file_path = user_data_dir / "zip_files" / match.zip_filename
if not zip_file_path.exists():
logger.warning(f"ZIP file not found: {zip_file_path}")
return
# Create temporary directory for extraction
temp_dir = Path(tempfile.mkdtemp(prefix=f"match_{match_id}_"))
logger.info(f"Extracting ZIP file {zip_file_path} to temporary directory: {temp_dir}")
# Extract the ZIP file
with zipfile.ZipFile(str(zip_file_path), 'r') as zip_ref:
zip_ref.extractall(str(temp_dir))
# Log extraction results
extracted_files = list(temp_dir.rglob("*"))
logger.info(f"Successfully extracted {len(extracted_files)} files from {match.zip_filename}")
# Store the temporary directory path for potential cleanup
# In a real implementation, you might want to track this for cleanup
match.temp_extract_path = str(temp_dir)
# Update match in database with temp path (optional)
session.commit()
logger.info(f"ZIP extraction completed for match {match_id}")
finally:
session.close()
except Exception as e:
logger.error(f"Failed to unzip ZIP file for match {match_id}: {e}")
def _cleanup(self):
"""Perform cleanup operations"""
try:
......
......@@ -64,7 +64,10 @@ class MessageType(Enum):
START_GAME = "START_GAME"
SCHEDULE_GAMES = "SCHEDULE_GAMES"
START_GAME_DELAYED = "START_GAME_DELAYED"
START_INTRO = "START_INTRO"
MATCH_START = "MATCH_START"
PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE"
GAME_STATUS = "GAME_STATUS"
GAME_UPDATE = "GAME_UPDATE"
......@@ -134,9 +137,10 @@ class Message:
class MessageBus:
"""Central message bus for inter-thread communication"""
def __init__(self, max_queue_size: int = 1000, dev_message: bool = False):
def __init__(self, max_queue_size: int = 1000, dev_message: bool = False, debug_messages: bool = False):
self.max_queue_size = max_queue_size
self.dev_message = dev_message
self.debug_messages = debug_messages
self._queues: Dict[str, Queue] = {}
self._handlers: Dict[str, Dict[MessageType, List[Callable]]] = {}
self._global_handlers: Dict[MessageType, List[Callable]] = {}
......@@ -147,6 +151,8 @@ class MessageBus:
if dev_message:
logger.info("MessageBus initialized with dev_message mode enabled")
elif debug_messages:
logger.info("MessageBus initialized with debug_messages mode enabled")
else:
logger.info("MessageBus initialized")
......@@ -207,6 +213,16 @@ class MessageBus:
# Log the message (only in dev_message mode)
if self.dev_message:
logger.info(f"📨 MESSAGE_BUS: {message}")
# Display message on screen (debug_messages mode with debug enabled)
if self.debug_messages and self.dev_message:
timestamp_str = datetime.fromtimestamp(message.timestamp).strftime('%H:%M:%S.%f')[:-3]
print(f"[{timestamp_str}] 📨 {message.sender} -> {message.recipient or 'ALL'}: {message.type.value}")
if message.data:
# Show key data fields (truncate long values)
data_str = ", ".join([f"{k}: {str(v)[:50]}{'...' if len(str(v)) > 50 else ''}" for k, v in message.data.items()])
print(f" Data: {{{data_str}}}")
print() # Empty line for readability
if broadcast or message.recipient is None:
# Broadcast to all components
......@@ -578,6 +594,18 @@ class MessageBuilder:
}
)
@staticmethod
def start_intro(sender: str, fixture_id: Optional[str] = None, match_id: Optional[int] = None) -> Message:
"""Create START_INTRO message"""
return Message(
type=MessageType.START_INTRO,
sender=sender,
data={
"fixture_id": fixture_id,
"match_id": match_id
}
)
@staticmethod
def match_start(sender: str, fixture_id: str, match_id: int) -> Message:
"""Create MATCH_START message"""
......@@ -588,4 +616,30 @@ class MessageBuilder:
"fixture_id": fixture_id,
"match_id": match_id
}
)
@staticmethod
def play_video_match(sender: str, match_id: int, video_filename: str, fixture_id: Optional[str] = None) -> Message:
"""Create PLAY_VIDEO_MATCH message"""
return Message(
type=MessageType.PLAY_VIDEO_MATCH,
sender=sender,
data={
"match_id": match_id,
"video_filename": video_filename,
"fixture_id": fixture_id
}
)
@staticmethod
def play_video_match_done(sender: str, match_id: int, video_filename: str, fixture_id: Optional[str] = None) -> Message:
"""Create PLAY_VIDEO_MATCH_DONE message"""
return Message(
type=MessageType.PLAY_VIDEO_MATCH_DONE,
sender=sender,
data={
"match_id": match_id,
"video_filename": video_filename,
"fixture_id": fixture_id
}
)
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -115,7 +115,7 @@
<h5>All Fixtures</h5>
<span id="filtered-count" class="badge bg-secondary">0 fixtures</span>
</div>
<div class="card-body p-0">
<div class="card-body p-0" style="padding-bottom: 100px !important;">
<div class="table-responsive">
<table class="table table-hover mb-0" id="fixtures-table">
<thead class="table-light">
......
......@@ -110,7 +110,12 @@
<i class="fas fa-shield-alt me-2"></i>Administrator Actions
</h6>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<button class="btn btn-outline-primary w-100" id="btn-upload-intro-video">
<i class="fas fa-upload me-2"></i>Upload Intro Video
</button>
</div>
<div class="col-md-4 mb-3">
<button class="btn btn-outline-danger w-100" id="btn-shutdown-app">
<i class="fas fa-power-off me-2"></i>Shutdown Application
</button>
......@@ -364,6 +369,51 @@
</div>
</div>
</div>
<!-- Upload Intro Video Modal -->
<div class="modal fade" id="uploadIntroVideoModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Upload Default Intro Video</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="upload-intro-video-form" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Select Video File</label>
<input type="file" class="form-control" id="intro-video-file"
accept="video/*" required>
<div class="form-text">
Supported formats: MP4, AVI, MOV, MKV, WebM (Max 500MB)
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="overwrite-existing">
<label class="form-check-label" for="overwrite-existing">
Overwrite existing intro video
</label>
</div>
</div>
<div id="upload-progress" class="d-none">
<div class="progress mb-2">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted">Uploading...</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-upload-intro-video">
<i class="fas fa-upload me-1"></i>Upload Video
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
......@@ -395,6 +445,10 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = '/tokens';
});
document.getElementById('btn-upload-intro-video').addEventListener('click', function() {
new bootstrap.Modal(document.getElementById('uploadIntroVideoModal')).show();
});
// Match interval save button
document.getElementById('btn-save-interval').addEventListener('click', function() {
saveMatchInterval();
......@@ -509,22 +563,22 @@ document.addEventListener('DOMContentLoaded', function() {
const icon = document.getElementById('message-icon').value || '📢';
const template = document.getElementById('message-template').value || 'text';
const displayTime = parseInt(document.getElementById('message-display-time').value) || 10;
if (!title.trim()) {
alert('Please enter a message title');
return;
}
if (!content.trim()) {
alert('Please enter message content');
return;
}
if (displayTime < 1 || displayTime > 300) {
alert('Display time must be between 1 and 300 seconds');
return;
}
fetch('/api/send-custom-message', {
method: 'POST',
headers: {
......@@ -556,6 +610,71 @@ document.addEventListener('DOMContentLoaded', function() {
alert('Error sending message: ' + error.message);
});
});
document.getElementById('confirm-upload-intro-video').addEventListener('click', function() {
const fileInput = document.getElementById('intro-video-file');
const file = fileInput.files[0];
const overwrite = document.getElementById('overwrite-existing').checked;
if (!file) {
alert('Please select a video file to upload');
return;
}
// Validate file size (500MB limit)
const maxSize = 500 * 1024 * 1024; // 500MB in bytes
if (file.size > maxSize) {
alert('File size must be less than 500MB');
return;
}
// Validate file type
const allowedTypes = ['video/mp4', 'video/avi', 'video/quicktime', 'video/x-matroska', 'video/webm'];
if (!allowedTypes.includes(file.type) && !file.name.match(/\.(mp4|avi|mov|mkv|webm)$/i)) {
alert('Please select a valid video file (MP4, AVI, MOV, MKV, or WebM)');
return;
}
const formData = new FormData();
formData.append('video_file', file);
formData.append('overwrite', overwrite);
const uploadBtn = document.getElementById('confirm-upload-intro-video');
const originalText = uploadBtn.innerHTML;
const progressDiv = document.getElementById('upload-progress');
const progressBar = progressDiv.querySelector('.progress-bar');
// Show progress and disable button
progressDiv.classList.remove('d-none');
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
fetch('/api/upload-intro-video', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('uploadIntroVideoModal')).hide();
// Clear the form
document.getElementById('upload-intro-video-form').reset();
alert('Intro video uploaded successfully!');
} else {
alert('Failed to upload video: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
alert('Error uploading video: ' + error.message);
})
.finally(() => {
// Hide progress and restore button
progressDiv.classList.add('d-none');
progressBar.style.width = '0%';
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalText;
});
});
// Status update functions
function updateSystemStatus() {
......
......@@ -69,13 +69,13 @@
<div class="col-md-6">
<label class="form-label">Default Show Time</label>
<input type="text" class="form-control" id="defaultShowTime"
placeholder="00:30" pattern="[0-9]{1,2}:[0-5][0-9]">
placeholder="00:15" pattern="[0-9]{1,2}:[0-5][0-9]">
<div class="form-text">Default display duration (MM:SS)</div>
</div>
<div class="col-md-6">
<label class="form-label">Rotating Interval</label>
<input type="text" class="form-control" id="rotatingTime"
placeholder="05:00" pattern="[0-9]{1,2}:[0-5][0-9]">
placeholder="00:15" pattern="[0-9]{1,2}:[0-5][0-9]">
<div class="form-text">Time between template rotations (MM:SS)</div>
</div>
</div>
......@@ -176,8 +176,8 @@
let availableTemplates = [];
let outcomeAssignments = {};
let introTemplates = [];
let defaultShowTime = '00:30';
let rotatingTime = '05:00';
let defaultShowTime = '00:15';
let rotatingTime = '00:15';
// Define all possible outcomes
const allOutcomes = [
......@@ -517,15 +517,54 @@
.then(response => response.json())
.then(data => {
introTemplates = data.templates || [];
defaultShowTime = data.default_show_time || '00:30';
rotatingTime = data.rotating_time || '05:00';
defaultShowTime = data.default_show_time || '00:15';
rotatingTime = data.rotating_time || '00:15';
// Set default intro templates if none exist
if (introTemplates.length === 0) {
// Check if fixtures and match templates are available
const fixturesTemplate = availableTemplates.find(t => t.name === 'fixtures');
const matchTemplate = availableTemplates.find(t => t.name === 'match');
if (fixturesTemplate) {
introTemplates.push({
name: 'fixtures',
show_time: '00:15'
});
}
if (matchTemplate) {
introTemplates.push({
name: 'match',
show_time: '00:15'
});
}
}
document.getElementById('defaultShowTime').value = defaultShowTime;
document.getElementById('rotatingTime').value = rotatingTime;
})
.catch(error => {
console.error('Error loading intro templates:', error);
introTemplates = [];
// Set default intro templates on error too
const fixturesTemplate = availableTemplates.find(t => t.name === 'fixtures');
const matchTemplate = availableTemplates.find(t => t.name === 'match');
if (fixturesTemplate) {
introTemplates.push({
name: 'fixtures',
show_time: '00:15'
});
}
if (matchTemplate) {
introTemplates.push({
name: 'match',
show_time: '00:15'
});
}
});
}
......@@ -696,8 +735,8 @@
const data = {
templates: introTemplates,
default_show_time: defaultTime || '00:30',
rotating_time: rotatingInterval || '05:00'
default_show_time: defaultTime || '00:15',
rotating_time: rotatingInterval || '00:15'
};
fetch('/api/intro-templates', {
......@@ -898,10 +937,6 @@
cursor: grabbing;
}
/* Add margin at bottom for better scrolling */
.container-fluid {
padding-bottom: 100px;
}
</style>
{% endblock %}
\ No newline at end of file
{% endblock %}
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