mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 13:23:00 +00:00
Feature/vegas scroll mode (#215)
* feat(display): add Vegas-style continuous scroll mode Implement an opt-in Vegas ticker mode that composes all enabled plugin content into a single continuous horizontal scroll. Includes a modular package (src/vegas_mode/) with double-buffered streaming, 125 FPS render pipeline using the existing ScrollHelper, live priority interruption support, and a web UI for configuration with drag-drop plugin ordering. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): add three-mode display system (SCROLL, FIXED_SEGMENT, STATIC) Adds a flexible display mode system for Vegas scroll mode that allows plugins to control how their content appears in the continuous scroll: - SCROLL: Content scrolls continuously (multi-item plugins like sports) - FIXED_SEGMENT: Fixed block that scrolls by (clock, weather) - STATIC: Scroll pauses, plugin displays, then resumes (alerts) Changes: - Add VegasDisplayMode enum to base_plugin.py with backward-compatible mapping from legacy get_vegas_content_type() - Add static pause handling to coordinator with scroll position save/restore - Add mode-aware content composition to stream_manager - Add vegas_mode info to /api/v3/plugins/installed endpoint - Add mode indicators to Vegas settings UI - Add comprehensive plugin developer documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas,widgets): address validation, thread safety, and XSS issues Vegas mode fixes: - config.py: align validation limits with UI (scroll_speed max 200, separator_width max 128) - coordinator.py: fix race condition by properly initializing _pending_config - plugin_adapter.py: remove unused import - render_pipeline.py: preserve deque type in reset() method - stream_manager.py: fix lock handling and swap_buffers to truly swap API fixes: - api_v3.py: normalize boolean checkbox values, validate numeric fields, ensure JSON arrays Widget fixes: - day-selector.js: remove escapeHtml from JSON.stringify to prevent corruption - password-input.js: use deterministic color class mapping for Tailwind JIT - radio-group.js: replace inline onchange with addEventListener to prevent XSS - select-dropdown.js: guard global registry access - slider.js: add escapeAttr for attributes, fix null dereference in setValue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve exception handling and static pause state management coordinator.py: - _check_live_priority: use logger.exception for full traceback - _end_static_pause: guard scroll resume on interruption (stop/live priority) - _update_static_mode_plugins: log errors instead of silently swallowing render_pipeline.py: - compose_scroll_content: use specific exceptions and logger.exception - render_frame: use specific exceptions and logger.exception - hot_swap_content: use specific exceptions and logger.exception Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add interrupt mechanism and improve config/exception handling - Add interrupt checker callback to Vegas coordinator for responsive handling of on-demand requests and wifi status during Vegas mode - Fix config.py update() to include dynamic duration fields - Fix is_plugin_included() consistency with get_ordered_plugins() - Update _apply_pending_config to propagate config to StreamManager - Change _fetch_plugin_content to use logger.exception for traceback - Replace bare except in _refresh_plugin_list with specific exceptions - Add aria-label accessibility to Vegas toggle checkbox - Fix XSS vulnerability in plugin metadata rendering with escapeHtml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve logging, validation, lock handling, and config updates - display_controller.py: use logger.exception for Vegas errors with traceback - base_plugin.py: validate vegas_panel_count as positive integer with warning - coordinator.py: fix _apply_pending_config to avoid losing concurrent updates by clearing _pending_config while holding lock - plugin_adapter.py: remove broad catch-all, use narrower exception types (AttributeError, TypeError, ValueError, OSError, RuntimeError) and logger.exception for traceback preservation - api_v3.py: only update vegas_config['enabled'] when key is present in data to prevent incorrect disabling when checkbox is omitted Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve cycle advancement, logging, and accessibility - Add advance_cycle() method to StreamManager for clearing buffer between cycles - Call advance_cycle() in RenderPipeline.start_new_cycle() for fresh content - Use logger.exception() for interrupt check and static pause errors (full tracebacks) - Add id="vegas_scroll_label" to h3 for aria-labelledby reference - Call updatePluginConfig() after rendering plugin list for proper initialization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add thread-safety, preserve updates, and improve logging - display_controller.py: Use logger.exception() for Vegas import errors - plugin_adapter.py: Add thread-safe cache lock, remove unused exception binding - stream_manager.py: In-place merge in process_updates() preserves non-updated plugins - api_v3.py: Change vegas_scroll_enabled default from False to True Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add debug logging and narrow exception types - stream_manager.py: Log when get_vegas_display_mode() is unavailable - stream_manager.py: Narrow exception type from Exception to (AttributeError, TypeError) - api_v3.py: Log exceptions when reading Vegas display metadata with plugin context Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): fix method call and improve exception logging - Fix _check_vegas_interrupt() calling nonexistent _check_wifi_status(), now correctly calls _check_wifi_status_message() - Update _refresh_plugin_list() exception handler to use logger.exception() with plugin_id and class name for remote debugging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(web): replace complex toggle with standard checkbox for Vegas mode The Tailwind pseudo-element toggle (after:content-[''], etc.) wasn't rendering because these classes weren't in the CSS bundle. Replaced with a simple checkbox that matches other form controls in the template. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug(vegas): add detailed logging to _refresh_plugin_list Track why plugins aren't being found for Vegas scroll: - Log count of loaded plugins - Log enabled status for each plugin - Log content_type and display_mode checks - Log when plugin_manager lacks loaded_plugins Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): use correct attribute name for plugin manager StreamManager and VegasModeCoordinator were checking for plugin_manager.loaded_plugins but PluginManager stores active plugins in plugin_manager.plugins. This caused Vegas scroll to find zero plugins despite plugins being available. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): convert scroll_speed from px/sec to px/frame correctly The config scroll_speed is in pixels per second, but ScrollHelper in frame_based_scrolling mode interprets it as pixels per frame. Previously this caused the speed to be clamped to max 5.0 regardless of the configured value. Now properly converts: pixels_per_frame = scroll_speed * scroll_delay With defaults (50 px/s, 0.02s delay), this gives 1 px/frame = 50 px/s. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): add FPS logging every 5 seconds Logs actual FPS vs target FPS to help diagnose performance issues. Shows frame count in each 5-second interval. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve plugin content capture reliability - Call update_data() before capture to ensure fresh plugin data - Try display() without force_clear first, fallback if TypeError - Retry capture with force_clear=True if first attempt is blank - Use histogram-based blank detection instead of point sampling (more reliable for content positioned anywhere in frame) This should help capture content from plugins that don't implement get_vegas_content() natively. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): handle callable width/height on display_manager DisplayManager.width and .height may be methods or properties depending on the implementation. Use callable() check to call them if needed, ensuring display_width and display_height are always integers. Fixes potential TypeError when width/height are methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): use logger.exception for display mode errors Replace logger.error with logger.exception to capture full stack trace when get_vegas_display_mode() fails on a plugin. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): protect plugin list updates with buffer lock Move assignment of _ordered_plugins and index resets under _buffer_lock to prevent race conditions with _prefetch_content() which reads these variables under the same lock. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): catch all exceptions in get_vegas_display_mode Broaden exception handling from AttributeError/TypeError to Exception so any plugin error in get_vegas_display_mode() doesn't abort the entire plugin list refresh. The loop continues with the default FIXED_SEGMENT mode. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): refresh stream manager when config updates After updating stream_manager.config, force a refresh to pick up changes to plugin_order, excluded_plugins, and buffer_ahead settings. Also use logger.exception to capture full stack traces on config update errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug(vegas): add detailed logging for blank image detection * feat(vegas): extract full scroll content from plugins using ScrollHelper Plugins like ledmatrix-stocks and odds-ticker use ScrollHelper with a cached_image that contains their full scrolling content. Instead of falling back to single-frame capture, now check for scroll_helper.cached_image first to get the complete scrolling content for Vegas mode. * debug(vegas): add comprehensive INFO-level logging for plugin content flow - Log each plugin being processed with class name - Log which content methods are tried (native, scroll_helper, fallback) - Log success/failure of each method with image dimensions - Log brightness check results for blank image detection - Add visual separators in logs for easier debugging - Log plugin list refresh with enabled/excluded status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): trigger scroll content generation when cache is empty When a plugin has a scroll_helper but its cached_image is not yet populated, try to trigger content generation by: 1. Calling _create_scrolling_display() if available (stocks pattern) 2. Calling display(force_clear=True) as a fallback This allows plugins like stocks to provide their full scroll content even when Vegas mode starts before the plugin has run its normal display cycle. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve exception handling in plugin_adapter scroll content retrieval Replace broad except Exception handlers with narrow exception types (AttributeError, TypeError, ValueError, OSError) and use logger.exception instead of logger.warning/info to capture full stack traces for better diagnosability. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: narrow exception handling in coordinator and plugin_adapter - coordinator.py: Replace broad Exception catch around get_vegas_display_mode() with (AttributeError, TypeError) and use logger.exception for stack traces - plugin_adapter.py: Narrow update_data() exception handler to (AttributeError, RuntimeError, OSError) and use logger.exception Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve Vegas mode robustness and API validation - display_controller: Guard against None plugin_manager in Vegas init - coordinator: Restore scrolling state in resume() to match pause() - api_v3: Validate Vegas numeric fields with range checks and 400 errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
655
src/vegas_mode/coordinator.py
Normal file
655
src/vegas_mode/coordinator.py
Normal file
@@ -0,0 +1,655 @@
|
||||
"""
|
||||
Vegas Mode Coordinator
|
||||
|
||||
Main orchestrator for Vegas-style continuous scroll mode. Coordinates between
|
||||
StreamManager, RenderPipeline, and the display system to provide smooth
|
||||
continuous scrolling of all enabled plugin content.
|
||||
|
||||
Supports three display modes per plugin:
|
||||
- SCROLL: Content scrolls continuously within the stream
|
||||
- FIXED_SEGMENT: Fixed block that scrolls by with other content
|
||||
- STATIC: Scroll pauses, plugin displays for its duration, then resumes
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
from typing import Optional, Dict, Any, List, Callable, TYPE_CHECKING
|
||||
|
||||
from src.vegas_mode.config import VegasModeConfig
|
||||
from src.vegas_mode.plugin_adapter import PluginAdapter
|
||||
from src.vegas_mode.stream_manager import StreamManager
|
||||
from src.vegas_mode.render_pipeline import RenderPipeline
|
||||
from src.plugin_system.base_plugin import VegasDisplayMode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.plugin_system.plugin_manager import PluginManager
|
||||
from src.plugin_system.base_plugin import BasePlugin
|
||||
from src.display_manager import DisplayManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VegasModeCoordinator:
|
||||
"""
|
||||
Orchestrates Vegas scroll mode operation.
|
||||
|
||||
Responsibilities:
|
||||
- Initialize and coordinate all Vegas mode components
|
||||
- Manage the high-FPS render loop
|
||||
- Handle live priority interruptions
|
||||
- Process config updates
|
||||
- Provide status and control interface
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Dict[str, Any],
|
||||
display_manager: 'DisplayManager',
|
||||
plugin_manager: 'PluginManager'
|
||||
):
|
||||
"""
|
||||
Initialize the Vegas mode coordinator.
|
||||
|
||||
Args:
|
||||
config: Main configuration dictionary
|
||||
display_manager: DisplayManager instance
|
||||
plugin_manager: PluginManager instance
|
||||
"""
|
||||
# Parse configuration
|
||||
self.vegas_config = VegasModeConfig.from_config(config)
|
||||
|
||||
# Store references
|
||||
self.display_manager = display_manager
|
||||
self.plugin_manager = plugin_manager
|
||||
|
||||
# Initialize components
|
||||
self.plugin_adapter = PluginAdapter(display_manager)
|
||||
self.stream_manager = StreamManager(
|
||||
self.vegas_config,
|
||||
plugin_manager,
|
||||
self.plugin_adapter
|
||||
)
|
||||
self.render_pipeline = RenderPipeline(
|
||||
self.vegas_config,
|
||||
display_manager,
|
||||
self.stream_manager
|
||||
)
|
||||
|
||||
# State management
|
||||
self._is_active = False
|
||||
self._is_paused = False
|
||||
self._should_stop = False
|
||||
self._state_lock = threading.Lock()
|
||||
|
||||
# Live priority tracking
|
||||
self._live_priority_active = False
|
||||
self._live_priority_check: Optional[Callable[[], Optional[str]]] = None
|
||||
|
||||
# Interrupt checker for yielding control back to display controller
|
||||
self._interrupt_check: Optional[Callable[[], bool]] = None
|
||||
self._interrupt_check_interval: int = 10 # Check every N frames
|
||||
|
||||
# Config update tracking
|
||||
self._config_version = 0
|
||||
self._pending_config_update = False
|
||||
self._pending_config: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Static pause handling
|
||||
self._static_pause_active = False
|
||||
self._static_pause_plugin: Optional['BasePlugin'] = None
|
||||
self._static_pause_start: Optional[float] = None
|
||||
self._saved_scroll_position: Optional[int] = None
|
||||
|
||||
# Track which plugins should use STATIC mode (pause scroll)
|
||||
self._static_mode_plugins: set = set()
|
||||
|
||||
# Statistics
|
||||
self.stats = {
|
||||
'total_runtime_seconds': 0.0,
|
||||
'cycles_completed': 0,
|
||||
'interruptions': 0,
|
||||
'config_updates': 0,
|
||||
'static_pauses': 0,
|
||||
}
|
||||
self._start_time: Optional[float] = None
|
||||
|
||||
logger.info(
|
||||
"VegasModeCoordinator initialized: enabled=%s, fps=%d, buffer_ahead=%d",
|
||||
self.vegas_config.enabled,
|
||||
self.vegas_config.target_fps,
|
||||
self.vegas_config.buffer_ahead
|
||||
)
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
"""Check if Vegas mode is enabled in configuration."""
|
||||
return self.vegas_config.enabled
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""Check if Vegas mode is currently running."""
|
||||
return self._is_active
|
||||
|
||||
def set_live_priority_checker(self, checker: Callable[[], Optional[str]]) -> None:
|
||||
"""
|
||||
Set the callback for checking live priority content.
|
||||
|
||||
Args:
|
||||
checker: Callable that returns live priority mode name or None
|
||||
"""
|
||||
self._live_priority_check = checker
|
||||
|
||||
def set_interrupt_checker(
|
||||
self,
|
||||
checker: Callable[[], bool],
|
||||
check_interval: int = 10
|
||||
) -> None:
|
||||
"""
|
||||
Set the callback for checking if Vegas should yield control.
|
||||
|
||||
This allows the display controller to interrupt Vegas mode
|
||||
when on-demand, wifi status, or other priority events occur.
|
||||
|
||||
Args:
|
||||
checker: Callable that returns True if Vegas should yield
|
||||
check_interval: Check every N frames (default 10)
|
||||
"""
|
||||
self._interrupt_check = checker
|
||||
self._interrupt_check_interval = max(1, check_interval)
|
||||
|
||||
def start(self) -> bool:
|
||||
"""
|
||||
Start Vegas mode operation.
|
||||
|
||||
Returns:
|
||||
True if started successfully
|
||||
"""
|
||||
if not self.vegas_config.enabled:
|
||||
logger.warning("Cannot start Vegas mode - not enabled in config")
|
||||
return False
|
||||
|
||||
with self._state_lock:
|
||||
if self._is_active:
|
||||
logger.warning("Vegas mode already active")
|
||||
return True
|
||||
|
||||
# Validate configuration
|
||||
errors = self.vegas_config.validate()
|
||||
if errors:
|
||||
logger.error("Vegas config validation failed: %s", errors)
|
||||
return False
|
||||
|
||||
# Initialize stream manager
|
||||
if not self.stream_manager.initialize():
|
||||
logger.error("Failed to initialize stream manager")
|
||||
return False
|
||||
|
||||
# Compose initial content
|
||||
if not self.render_pipeline.compose_scroll_content():
|
||||
logger.error("Failed to compose initial scroll content")
|
||||
return False
|
||||
|
||||
self._is_active = True
|
||||
self._should_stop = False
|
||||
self._start_time = time.time()
|
||||
|
||||
logger.info("Vegas mode started")
|
||||
return True
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop Vegas mode operation."""
|
||||
with self._state_lock:
|
||||
if not self._is_active:
|
||||
return
|
||||
|
||||
self._should_stop = True
|
||||
self._is_active = False
|
||||
|
||||
if self._start_time:
|
||||
self.stats['total_runtime_seconds'] += time.time() - self._start_time
|
||||
self._start_time = None
|
||||
|
||||
# Cleanup components
|
||||
self.render_pipeline.reset()
|
||||
self.stream_manager.reset()
|
||||
self.display_manager.set_scrolling_state(False)
|
||||
|
||||
logger.info("Vegas mode stopped")
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause Vegas mode (for live priority interruption)."""
|
||||
with self._state_lock:
|
||||
if not self._is_active:
|
||||
return
|
||||
self._is_paused = True
|
||||
self.stats['interruptions'] += 1
|
||||
|
||||
self.display_manager.set_scrolling_state(False)
|
||||
logger.info("Vegas mode paused")
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume Vegas mode after pause."""
|
||||
with self._state_lock:
|
||||
if not self._is_active:
|
||||
return
|
||||
self._is_paused = False
|
||||
|
||||
self.display_manager.set_scrolling_state(True)
|
||||
logger.info("Vegas mode resumed")
|
||||
|
||||
def run_frame(self) -> bool:
|
||||
"""
|
||||
Run a single frame of Vegas mode.
|
||||
|
||||
Should be called at target FPS (e.g., 125 FPS = every 8ms).
|
||||
|
||||
Returns:
|
||||
True if frame was rendered, False if Vegas mode is not active
|
||||
"""
|
||||
# Check if we should be running
|
||||
with self._state_lock:
|
||||
if not self._is_active or self._is_paused or self._should_stop:
|
||||
return False
|
||||
# Check for config updates (synchronized access)
|
||||
has_pending_update = self._pending_config_update
|
||||
|
||||
# Check for live priority
|
||||
if self._check_live_priority():
|
||||
return False
|
||||
|
||||
# Apply pending config update outside lock
|
||||
if has_pending_update:
|
||||
self._apply_pending_config()
|
||||
|
||||
# Check if we need to start a new cycle
|
||||
if self.render_pipeline.is_cycle_complete():
|
||||
if not self.render_pipeline.start_new_cycle():
|
||||
logger.warning("Failed to start new Vegas cycle")
|
||||
return False
|
||||
self.stats['cycles_completed'] += 1
|
||||
|
||||
# Check for hot-swap opportunities
|
||||
if self.render_pipeline.should_recompose():
|
||||
self.render_pipeline.hot_swap_content()
|
||||
|
||||
# Render frame
|
||||
return self.render_pipeline.render_frame()
|
||||
|
||||
def run_iteration(self) -> bool:
|
||||
"""
|
||||
Run a complete Vegas mode iteration (display duration).
|
||||
|
||||
This is called by DisplayController to run Vegas mode for one
|
||||
"display duration" period before checking for mode changes.
|
||||
|
||||
Handles three display modes:
|
||||
- SCROLL/FIXED_SEGMENT: Continue normal scroll rendering
|
||||
- STATIC: Pause scroll, display plugin, resume on completion
|
||||
|
||||
Returns:
|
||||
True if iteration completed normally, False if interrupted
|
||||
"""
|
||||
if not self.is_active:
|
||||
if not self.start():
|
||||
return False
|
||||
|
||||
# Update static mode plugin list on iteration start
|
||||
self._update_static_mode_plugins()
|
||||
|
||||
frame_interval = self.vegas_config.get_frame_interval()
|
||||
duration = self.render_pipeline.get_dynamic_duration()
|
||||
start_time = time.time()
|
||||
frame_count = 0
|
||||
fps_log_interval = 5.0 # Log FPS every 5 seconds
|
||||
last_fps_log_time = start_time
|
||||
fps_frame_count = 0
|
||||
|
||||
logger.info("Starting Vegas iteration for %.1fs", duration)
|
||||
|
||||
while True:
|
||||
# Check for STATIC mode plugin that should pause scroll
|
||||
static_plugin = self._check_static_plugin_trigger()
|
||||
if static_plugin:
|
||||
if not self._handle_static_pause(static_plugin):
|
||||
# Static pause was interrupted
|
||||
return False
|
||||
# After static pause, skip this segment and continue
|
||||
self.stream_manager.get_next_segment() # Consume the segment
|
||||
continue
|
||||
|
||||
# Run frame
|
||||
if not self.run_frame():
|
||||
# Check why we stopped
|
||||
with self._state_lock:
|
||||
if self._should_stop:
|
||||
return False
|
||||
if self._is_paused:
|
||||
# Paused for live priority - let caller handle
|
||||
return False
|
||||
|
||||
# Sleep for frame interval
|
||||
time.sleep(frame_interval)
|
||||
|
||||
# Increment frame count and check for interrupt periodically
|
||||
frame_count += 1
|
||||
fps_frame_count += 1
|
||||
|
||||
# Periodic FPS logging
|
||||
current_time = time.time()
|
||||
if current_time - last_fps_log_time >= fps_log_interval:
|
||||
fps = fps_frame_count / (current_time - last_fps_log_time)
|
||||
logger.info(
|
||||
"Vegas FPS: %.1f (target: %d, frames: %d)",
|
||||
fps, self.vegas_config.target_fps, fps_frame_count
|
||||
)
|
||||
last_fps_log_time = current_time
|
||||
fps_frame_count = 0
|
||||
|
||||
if (self._interrupt_check and
|
||||
frame_count % self._interrupt_check_interval == 0):
|
||||
try:
|
||||
if self._interrupt_check():
|
||||
logger.debug(
|
||||
"Vegas interrupted by callback after %d frames",
|
||||
frame_count
|
||||
)
|
||||
return False
|
||||
except Exception:
|
||||
# Log but don't let interrupt check errors stop Vegas
|
||||
logger.exception("Interrupt check failed")
|
||||
|
||||
# Check elapsed time
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed >= duration:
|
||||
break
|
||||
|
||||
# Check for cycle completion
|
||||
if self.render_pipeline.is_cycle_complete():
|
||||
break
|
||||
|
||||
logger.info("Vegas iteration completed after %.1fs", time.time() - start_time)
|
||||
return True
|
||||
|
||||
def _check_live_priority(self) -> bool:
|
||||
"""
|
||||
Check if live priority content should interrupt Vegas mode.
|
||||
|
||||
Returns:
|
||||
True if Vegas mode should be paused for live priority
|
||||
"""
|
||||
if not self._live_priority_check:
|
||||
return False
|
||||
|
||||
try:
|
||||
live_mode = self._live_priority_check()
|
||||
if live_mode:
|
||||
if not self._live_priority_active:
|
||||
self._live_priority_active = True
|
||||
self.pause()
|
||||
logger.info("Live priority detected: %s - pausing Vegas", live_mode)
|
||||
return True
|
||||
else:
|
||||
if self._live_priority_active:
|
||||
self._live_priority_active = False
|
||||
self.resume()
|
||||
logger.info("Live priority ended - resuming Vegas")
|
||||
return False
|
||||
except Exception:
|
||||
logger.exception("Error checking live priority")
|
||||
return False
|
||||
|
||||
def update_config(self, new_config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Update Vegas mode configuration.
|
||||
|
||||
Config changes are applied at next safe point to avoid disruption.
|
||||
|
||||
Args:
|
||||
new_config: New configuration dictionary
|
||||
"""
|
||||
with self._state_lock:
|
||||
self._pending_config_update = True
|
||||
self._pending_config = new_config
|
||||
self._config_version += 1
|
||||
self.stats['config_updates'] += 1
|
||||
|
||||
logger.debug("Config update queued (version %d)", self._config_version)
|
||||
|
||||
def _apply_pending_config(self) -> None:
|
||||
"""Apply pending configuration update."""
|
||||
# Atomically grab pending config and clear it to avoid losing concurrent updates
|
||||
with self._state_lock:
|
||||
if self._pending_config is None:
|
||||
self._pending_config_update = False
|
||||
return
|
||||
pending_config = self._pending_config
|
||||
self._pending_config = None # Clear while holding lock
|
||||
|
||||
try:
|
||||
new_vegas_config = VegasModeConfig.from_config(pending_config)
|
||||
|
||||
# Check if enabled state changed
|
||||
was_enabled = self.vegas_config.enabled
|
||||
self.vegas_config = new_vegas_config
|
||||
|
||||
# Update components
|
||||
self.render_pipeline.update_config(new_vegas_config)
|
||||
self.stream_manager.config = new_vegas_config
|
||||
|
||||
# Force refresh of stream manager to pick up plugin_order/buffer changes
|
||||
self.stream_manager._last_refresh = 0
|
||||
self.stream_manager.refresh()
|
||||
|
||||
# Handle enable/disable
|
||||
if was_enabled and not new_vegas_config.enabled:
|
||||
self.stop()
|
||||
elif not was_enabled and new_vegas_config.enabled:
|
||||
self.start()
|
||||
|
||||
logger.info("Config update applied (version %d)", self._config_version)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error applying config update")
|
||||
|
||||
finally:
|
||||
# Only clear update flag if no new config arrived during processing
|
||||
with self._state_lock:
|
||||
if self._pending_config is None:
|
||||
self._pending_config_update = False
|
||||
|
||||
def mark_plugin_updated(self, plugin_id: str) -> None:
|
||||
"""
|
||||
Notify that a plugin's data has been updated.
|
||||
|
||||
Args:
|
||||
plugin_id: ID of plugin that was updated
|
||||
"""
|
||||
if self._is_active:
|
||||
self.stream_manager.mark_plugin_updated(plugin_id)
|
||||
self.plugin_adapter.invalidate_cache(plugin_id)
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive Vegas mode status."""
|
||||
status = {
|
||||
'enabled': self.vegas_config.enabled,
|
||||
'active': self._is_active,
|
||||
'paused': self._is_paused,
|
||||
'live_priority_active': self._live_priority_active,
|
||||
'config': self.vegas_config.to_dict(),
|
||||
'stats': self.stats.copy(),
|
||||
}
|
||||
|
||||
if self._is_active:
|
||||
status['render_info'] = self.render_pipeline.get_current_scroll_info()
|
||||
status['stream_status'] = self.stream_manager.get_buffer_status()
|
||||
|
||||
return status
|
||||
|
||||
def get_ordered_plugins(self) -> List[str]:
|
||||
"""Get the current ordered list of plugins in Vegas scroll."""
|
||||
if hasattr(self.plugin_manager, 'plugins'):
|
||||
available = list(self.plugin_manager.plugins.keys())
|
||||
return self.vegas_config.get_ordered_plugins(available)
|
||||
return []
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Static pause handling (for STATIC display mode)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _check_static_plugin_trigger(self) -> Optional['BasePlugin']:
|
||||
"""
|
||||
Check if a STATIC mode plugin should take over display.
|
||||
|
||||
Called during iteration to detect when scroll should pause
|
||||
for a static plugin display.
|
||||
|
||||
Returns:
|
||||
Plugin instance if static pause should begin, None otherwise
|
||||
"""
|
||||
# Get the next plugin that would be displayed
|
||||
next_segment = self.stream_manager.peek_next_segment()
|
||||
if not next_segment:
|
||||
return None
|
||||
|
||||
plugin_id = next_segment.plugin_id
|
||||
plugin = self.plugin_manager.get_plugin(plugin_id)
|
||||
|
||||
if not plugin:
|
||||
return None
|
||||
|
||||
# Check if this plugin is configured for STATIC mode
|
||||
try:
|
||||
display_mode = plugin.get_vegas_display_mode()
|
||||
if display_mode == VegasDisplayMode.STATIC:
|
||||
return plugin
|
||||
except (AttributeError, TypeError):
|
||||
logger.exception("Error checking vegas mode for %s", plugin_id)
|
||||
|
||||
return None
|
||||
|
||||
def _handle_static_pause(self, plugin: 'BasePlugin') -> bool:
|
||||
"""
|
||||
Handle a static pause - scroll pauses while plugin displays.
|
||||
|
||||
Args:
|
||||
plugin: The STATIC mode plugin to display
|
||||
|
||||
Returns:
|
||||
True if completed normally, False if interrupted
|
||||
"""
|
||||
plugin_id = plugin.plugin_id
|
||||
|
||||
with self._state_lock:
|
||||
if self._static_pause_active:
|
||||
logger.warning("Static pause already active")
|
||||
return True
|
||||
|
||||
# Save current scroll position for smooth resume
|
||||
self._saved_scroll_position = self.render_pipeline.get_scroll_position()
|
||||
self._static_pause_active = True
|
||||
self._static_pause_plugin = plugin
|
||||
self._static_pause_start = time.time()
|
||||
self.stats['static_pauses'] += 1
|
||||
|
||||
logger.info("Static pause started for plugin: %s", plugin_id)
|
||||
|
||||
# Stop scrolling indicator
|
||||
self.display_manager.set_scrolling_state(False)
|
||||
|
||||
try:
|
||||
# Display the plugin using its standard display() method
|
||||
plugin.display(force_clear=True)
|
||||
self.display_manager.update_display()
|
||||
|
||||
# Wait for the plugin's display duration
|
||||
duration = plugin.get_display_duration()
|
||||
start = time.time()
|
||||
|
||||
while time.time() - start < duration:
|
||||
# Check for interruptions
|
||||
if self._should_stop:
|
||||
logger.info("Static pause interrupted by stop request")
|
||||
return False
|
||||
|
||||
if self._check_live_priority():
|
||||
logger.info("Static pause interrupted by live priority")
|
||||
return False
|
||||
|
||||
# Sleep in small increments to remain responsive
|
||||
time.sleep(0.1)
|
||||
|
||||
logger.info(
|
||||
"Static pause completed for %s after %.1fs",
|
||||
plugin_id, time.time() - start
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error during static pause for %s", plugin_id)
|
||||
return False
|
||||
|
||||
finally:
|
||||
self._end_static_pause()
|
||||
|
||||
return True
|
||||
|
||||
def _end_static_pause(self) -> None:
|
||||
"""End static pause and restore scroll state."""
|
||||
should_resume_scrolling = False
|
||||
|
||||
with self._state_lock:
|
||||
# Only resume scrolling if we weren't interrupted
|
||||
was_active = self._static_pause_active
|
||||
should_resume_scrolling = (
|
||||
was_active and
|
||||
not self._should_stop and
|
||||
not self._live_priority_active
|
||||
)
|
||||
|
||||
# Clear pause state
|
||||
self._static_pause_active = False
|
||||
self._static_pause_plugin = None
|
||||
self._static_pause_start = None
|
||||
|
||||
# Restore scroll position if we're resuming
|
||||
if should_resume_scrolling and self._saved_scroll_position is not None:
|
||||
self.render_pipeline.set_scroll_position(self._saved_scroll_position)
|
||||
self._saved_scroll_position = None
|
||||
|
||||
# Only resume scrolling state if not interrupted
|
||||
if should_resume_scrolling:
|
||||
self.display_manager.set_scrolling_state(True)
|
||||
logger.debug("Static pause ended, scroll resumed")
|
||||
else:
|
||||
logger.debug("Static pause ended (interrupted, not resuming scroll)")
|
||||
|
||||
def _update_static_mode_plugins(self) -> None:
|
||||
"""Update the set of plugins using STATIC display mode."""
|
||||
self._static_mode_plugins.clear()
|
||||
|
||||
for plugin_id in self.get_ordered_plugins():
|
||||
plugin = self.plugin_manager.get_plugin(plugin_id)
|
||||
if plugin:
|
||||
try:
|
||||
mode = plugin.get_vegas_display_mode()
|
||||
if mode == VegasDisplayMode.STATIC:
|
||||
self._static_mode_plugins.add(plugin_id)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Error getting vegas display mode for plugin %s",
|
||||
plugin_id
|
||||
)
|
||||
|
||||
if self._static_mode_plugins:
|
||||
logger.info(
|
||||
"Static mode plugins: %s",
|
||||
', '.join(self._static_mode_plugins)
|
||||
)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up all resources."""
|
||||
self.stop()
|
||||
self.render_pipeline.cleanup()
|
||||
self.stream_manager.cleanup()
|
||||
self.plugin_adapter.cleanup()
|
||||
logger.info("VegasModeCoordinator cleanup complete")
|
||||
Reference in New Issue
Block a user