763 lines
30 KiB
Python
763 lines
30 KiB
Python
# mpv_seamless_player.py - Enhanced with folder sequence support
|
|
"""
|
|
MPV-based seamless video player with folder sequence playback
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import random
|
|
import time
|
|
import threading
|
|
import logging
|
|
import queue
|
|
from pathlib import Path
|
|
import tkinter as tk
|
|
|
|
try:
|
|
import mpv
|
|
MPV_AVAILABLE = True
|
|
except ImportError:
|
|
MPV_AVAILABLE = False
|
|
print("ERROR: python-mpv not installed!")
|
|
|
|
class MPVSeamlessPlayer:
|
|
def __init__(self, config, debug_console):
|
|
self.config = config
|
|
self.debug_console = debug_console
|
|
self.logger = logging.getLogger(__name__)
|
|
self.running = False
|
|
|
|
if not MPV_AVAILABLE:
|
|
raise Exception("MPV not available")
|
|
|
|
# Video state
|
|
self.current_mode = "trailers"
|
|
self.current_video_path = None
|
|
self.specific_video_playing = False
|
|
self.specific_video_start_time = None
|
|
|
|
# MPV setup
|
|
self.player_a = None
|
|
self.player_b = None
|
|
self.active_player = 'a'
|
|
|
|
# Display settings
|
|
self.screen_width = 1920
|
|
self.screen_height = 1080
|
|
self.is_fullscreen = True
|
|
|
|
# Content - Enhanced with folder support
|
|
self.trailer_videos = []
|
|
self.key_mapping = {} # Single file mappings
|
|
self.folder_mapping = {} # Folder sequence mappings
|
|
self.folder_state = {} # Track current position in each folder
|
|
self.video_queue = queue.Queue()
|
|
self.lock = threading.Lock()
|
|
|
|
# Enhanced trailer randomness - avoid recent repeats
|
|
self.recently_played_trailers = [] # Track last N trailers
|
|
self.max_recent_trailers = 36 # Don't repeat within last 25 plays
|
|
self.trailer_history_size = 70 # Keep history of 50 for better randomness
|
|
|
|
# Transition state
|
|
self.transition_lock = threading.Lock()
|
|
self.last_transition_time = 0
|
|
|
|
# Control flags
|
|
self.force_next = False
|
|
self.pending_specific_video = False
|
|
|
|
# Minimum play time protection
|
|
self.video_start_time = None
|
|
self.minimum_play_time = 3.0
|
|
self.minimum_specific_play_time = 5.0
|
|
|
|
# Performance tracking
|
|
self.stats = {
|
|
'transitions': 0,
|
|
'failed_loads': 0,
|
|
'queue_processed': 0,
|
|
'folder_videos_played': 0
|
|
}
|
|
|
|
# Initialize
|
|
self.logger.info("=== MPV PLAYER INITIALIZATION ===")
|
|
self.detect_display_settings()
|
|
|
|
if not self.setup_mpv():
|
|
raise Exception("MPV setup failed")
|
|
|
|
self.load_content()
|
|
self.logger.info("=== MPV PLAYER READY ===")
|
|
|
|
def detect_display_settings(self):
|
|
"""Detect optimal display configuration"""
|
|
try:
|
|
temp_root = tk.Tk()
|
|
temp_root.withdraw()
|
|
|
|
self.screen_width = temp_root.winfo_screenwidth()
|
|
self.screen_height = temp_root.winfo_screenheight()
|
|
self.logger.info(f"Screen: {self.screen_width}x{self.screen_height}")
|
|
|
|
temp_root.destroy()
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Display detection failed: {e}")
|
|
self.screen_width = 1920
|
|
self.screen_height = 1080
|
|
|
|
def setup_mpv(self):
|
|
"""Initialize MPV players"""
|
|
try:
|
|
self.logger.info("Setting up MPV players...")
|
|
|
|
mpv_config = {
|
|
'vo': 'gpu',
|
|
'hwdec': 'auto',
|
|
'fullscreen': True,
|
|
'scale': 'lanczos',
|
|
'keepaspect': True,
|
|
'keepaspect-window': True,
|
|
'volume': 70,
|
|
'volume-max': 100,
|
|
'keep-open': 'no',
|
|
'loop-file': 'no',
|
|
'pause': False,
|
|
'osd-level': 0,
|
|
'quiet': True,
|
|
}
|
|
|
|
self.player_a = mpv.MPV(**mpv_config)
|
|
self.player_b = mpv.MPV(**mpv_config)
|
|
|
|
self.player_a.register_event_callback(lambda event: self._mpv_event_handler('a', event))
|
|
self.player_b.register_event_callback(lambda event: self._mpv_event_handler('b', event))
|
|
|
|
self.logger.info("MPV setup complete")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"MPV setup failed: {e}", exc_info=True)
|
|
return False
|
|
|
|
def _mpv_event_handler(self, player_id, event):
|
|
"""Handle MPV events for seamless playback - FIXED"""
|
|
try:
|
|
# MPV events don't have .get() - use direct attribute access
|
|
event_id = None
|
|
|
|
# Try to get event_id
|
|
if hasattr(event, 'event_id'):
|
|
event_id = event.event_id
|
|
elif isinstance(event, dict) and 'event_id' in event:
|
|
event_id = event['event_id']
|
|
|
|
# Event ID 7 = END_FILE
|
|
if event_id == 7:
|
|
if self.active_player == player_id and self.running:
|
|
self.logger.info(f"Video ended on player {player_id}")
|
|
self._handle_video_end()
|
|
|
|
except Exception as e:
|
|
# Silently ignore common event attribute errors
|
|
error_msg = str(e)
|
|
if "get" not in error_msg and "event_id" not in error_msg:
|
|
self.logger.debug(f"Event error for player {player_id}: {e}")
|
|
|
|
|
|
def _handle_video_end(self):
|
|
"""Handle natural video end"""
|
|
try:
|
|
if self.specific_video_playing:
|
|
self.logger.info("Specific video completed - returning to trailer mode")
|
|
self.debug_console.log("Video completed - switching back to trailer mode")
|
|
self.specific_video_playing = False
|
|
self.specific_video_start_time = None
|
|
self.current_mode = "trailers"
|
|
|
|
def queue_next_trailer():
|
|
time.sleep(self.config.TRANSITION_DELAY)
|
|
next_trailer = self.get_random_trailer()
|
|
if next_trailer and self.running:
|
|
self.video_queue.put(("trailers", next_trailer))
|
|
|
|
threading.Thread(target=queue_next_trailer, daemon=True).start()
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error handling video end: {e}")
|
|
|
|
def load_content(self):
|
|
"""Load video content and mappings - Enhanced with folder support"""
|
|
try:
|
|
self.logger.info("Loading video content...")
|
|
|
|
# Load trailers
|
|
trailers_path = Path(self.config.TRAILERS_DIR)
|
|
if trailers_path.exists():
|
|
for f in trailers_path.iterdir():
|
|
if f.is_file() and f.suffix.lower() in self.config.SUPPORTED_FORMATS:
|
|
self.trailer_videos.append(str(f))
|
|
self.logger.info(f"Loaded {len(self.trailer_videos)} trailer videos")
|
|
|
|
# Load single file mappings
|
|
self.key_mapping = self.config.load_key_mapping()
|
|
self.logger.info(f"Loaded {len(self.key_mapping)} single-file NFC mappings")
|
|
|
|
# Load folder mappings
|
|
self.folder_mapping = self.config.load_folder_mapping()
|
|
self.logger.info(f"Loaded {len(self.folder_mapping)} folder NFC mappings")
|
|
|
|
# Load folder state (playback positions)
|
|
self.folder_state = self.config.load_folder_state()
|
|
self.logger.info(f"Loaded playback state for {len(self.folder_state)} folders")
|
|
|
|
# Initialize state for new folders
|
|
for nfc_id, folder_data in self.folder_mapping.items():
|
|
folder_name = folder_data['folder_name']
|
|
if folder_name not in self.folder_state:
|
|
self.folder_state[folder_name] = 0
|
|
self.logger.info(f"Initialized folder '{folder_name}' at position 0")
|
|
|
|
# Save initial state
|
|
self.config.save_folder_state(self.folder_state)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Content loading error: {e}", exc_info=True)
|
|
|
|
def reset_all_folder_positions(self):
|
|
"""Reset all folder sequences back to the first video"""
|
|
try:
|
|
self.logger.info("Resetting all folder positions to start")
|
|
|
|
# Reset all positions to 0
|
|
for folder_name in self.folder_state:
|
|
self.folder_state[folder_name] = 0
|
|
|
|
# Save the reset state
|
|
self.config.save_folder_state(self.folder_state)
|
|
|
|
self.logger.info(f"Reset complete - {len(self.folder_state)} folders reset")
|
|
self.debug_console.log(f"All folder sequences reset to beginning ({len(self.folder_state)} folders)")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to reset folder positions: {e}")
|
|
self.debug_console.log_error("Failed to reset folder positions")
|
|
return False
|
|
|
|
def get_next_video_from_folder(self, nfc_id):
|
|
"""Get the next video in sequence from a folder"""
|
|
try:
|
|
if nfc_id not in self.folder_mapping:
|
|
return None
|
|
|
|
folder_data = self.folder_mapping[nfc_id]
|
|
folder_name = folder_data['folder_name']
|
|
videos = folder_data['videos']
|
|
|
|
if not videos:
|
|
self.logger.warning(f"No videos in folder '{folder_name}'")
|
|
return None
|
|
|
|
# Get current position
|
|
current_pos = self.folder_state.get(folder_name, 0)
|
|
|
|
# Ensure position is valid
|
|
if current_pos >= len(videos):
|
|
current_pos = 0
|
|
|
|
# Get the video at current position
|
|
video_path = videos[current_pos]
|
|
|
|
# Advance position for next time
|
|
next_pos = (current_pos + 1) % len(videos)
|
|
self.folder_state[folder_name] = next_pos
|
|
|
|
# Save state
|
|
self.config.save_folder_state(self.folder_state)
|
|
|
|
self.logger.info(f"Folder '{folder_name}': Playing video {current_pos + 1}/{len(videos)}")
|
|
self.logger.info(f"Next scan will play video {next_pos + 1}/{len(videos)}")
|
|
|
|
self.debug_console.log(f"Folder '{folder_name}': Video {current_pos + 1}/{len(videos)}")
|
|
|
|
return video_path
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting next folder video: {e}")
|
|
return None
|
|
|
|
def get_active_player(self):
|
|
"""Get currently active MPV player"""
|
|
return self.player_a if self.active_player == 'a' else self.player_b
|
|
|
|
def get_inactive_player(self):
|
|
"""Get currently inactive MPV player"""
|
|
return self.player_b if self.active_player == 'a' else self.player_a
|
|
|
|
def can_skip_video(self):
|
|
"""Check if video has played long enough to be skipped"""
|
|
if not self.video_start_time:
|
|
return True
|
|
|
|
current_time = time.time()
|
|
play_duration = current_time - self.video_start_time
|
|
|
|
min_time = self.minimum_specific_play_time if self.specific_video_playing else self.minimum_play_time
|
|
|
|
can_skip = play_duration >= min_time
|
|
|
|
if not can_skip:
|
|
remaining = min_time - play_duration
|
|
self.logger.debug(f"Skip blocked: needs {remaining:.1f}s more")
|
|
|
|
return can_skip
|
|
|
|
def seamless_transition(self, video_path, is_specific=False):
|
|
"""Perform seamless video transition"""
|
|
transition_start = time.time()
|
|
|
|
with self.transition_lock:
|
|
try:
|
|
video_name = Path(video_path).name
|
|
self.logger.info(f"Starting transition to: {video_name}")
|
|
|
|
# Rate limiting
|
|
time_since_last = time.time() - self.last_transition_time
|
|
if time_since_last < 0.3:
|
|
time.sleep(0.3 - time_since_last)
|
|
|
|
inactive_player = self.get_inactive_player()
|
|
active_player = self.get_active_player()
|
|
|
|
inactive_player.play(str(video_path))
|
|
|
|
# Wait for video to start
|
|
start_wait = time.time()
|
|
while time.time() - start_wait < 2.0:
|
|
try:
|
|
if not inactive_player.idle_active:
|
|
break
|
|
except:
|
|
pass
|
|
time.sleep(0.05)
|
|
|
|
time.sleep(0.15)
|
|
|
|
try:
|
|
if active_player:
|
|
active_player.stop()
|
|
except:
|
|
pass
|
|
|
|
self.active_player = 'b' if self.active_player == 'a' else 'a'
|
|
|
|
self.current_video_path = video_path
|
|
self.specific_video_playing = is_specific
|
|
self.last_transition_time = time.time()
|
|
self.stats['transitions'] += 1
|
|
self.video_start_time = time.time()
|
|
|
|
if is_specific:
|
|
self.specific_video_start_time = time.time()
|
|
self.stats['folder_videos_played'] += 1
|
|
self.logger.info(f"SPECIFIC VIDEO NOW PLAYING: {video_name}")
|
|
else:
|
|
self.specific_video_start_time = None
|
|
|
|
transition_time = time.time() - transition_start
|
|
self.logger.info(f"Transition complete in {transition_time:.3f}s")
|
|
|
|
self.debug_console.log_video_played(video_name)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.stats['failed_loads'] += 1
|
|
self.logger.error(f"Transition failed: {e}", exc_info=True)
|
|
return False
|
|
|
|
def skip_current_video(self):
|
|
"""Skip current video"""
|
|
try:
|
|
if not self.can_skip_video():
|
|
self.logger.info("Skip request ignored - minimum play time not reached")
|
|
return
|
|
|
|
self.logger.info("Skipping current video")
|
|
|
|
active_player = self.get_active_player()
|
|
try:
|
|
active_player.stop()
|
|
except:
|
|
pass
|
|
|
|
self.specific_video_playing = False
|
|
self.specific_video_start_time = None
|
|
self.pending_specific_video = False
|
|
self.force_next = True
|
|
self.video_start_time = None
|
|
|
|
self.debug_console.log("Video skipped by user")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Skip failed: {e}", exc_info=True)
|
|
|
|
def force_next_video(self):
|
|
"""Force next video"""
|
|
try:
|
|
if not self.can_skip_video():
|
|
return
|
|
|
|
self.logger.info("Forcing next video")
|
|
|
|
if not self.pending_specific_video:
|
|
self.force_next = True
|
|
self.specific_video_playing = False
|
|
self.specific_video_start_time = None
|
|
self.video_start_time = None
|
|
|
|
if self.current_mode == "specific":
|
|
self.current_mode = "trailers"
|
|
self.logger.info("Force returning to trailer mode")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Force next failed: {e}", exc_info=True)
|
|
|
|
def set_volume(self, volume):
|
|
"""Set volume for both MPV players"""
|
|
try:
|
|
vol = int(volume * 100)
|
|
vol = max(0, min(100, vol))
|
|
|
|
if self.player_a:
|
|
self.player_a.volume = vol
|
|
if self.player_b:
|
|
self.player_b.volume = vol
|
|
|
|
self.logger.debug(f"Volume set to {vol}%")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Volume setting failed: {e}")
|
|
|
|
def toggle_fullscreen(self):
|
|
"""Toggle fullscreen mode"""
|
|
try:
|
|
active_player = self.get_active_player()
|
|
if active_player:
|
|
try:
|
|
current_state = active_player.fullscreen
|
|
new_state = not current_state
|
|
active_player.fullscreen = new_state
|
|
|
|
self.is_fullscreen = new_state
|
|
self.logger.info(f"Fullscreen toggled: {new_state}")
|
|
self.debug_console.log(f"Fullscreen: {new_state}")
|
|
|
|
except Exception as e:
|
|
self.logger.warning(f"Fullscreen toggle failed: {e}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Fullscreen toggle failed: {e}")
|
|
|
|
def is_video_playing(self):
|
|
"""Check if video is currently playing"""
|
|
try:
|
|
active_player = self.get_active_player()
|
|
if not active_player:
|
|
return False
|
|
return not active_player.idle_active
|
|
except:
|
|
return False
|
|
|
|
def get_video_duration(self):
|
|
"""Get current video duration in seconds"""
|
|
try:
|
|
active_player = self.get_active_player()
|
|
if not active_player:
|
|
return 0
|
|
duration = active_player.duration
|
|
return duration if duration and duration > 0 else 0
|
|
except:
|
|
return 0
|
|
|
|
def get_video_position(self):
|
|
"""Get current video position in seconds"""
|
|
try:
|
|
active_player = self.get_active_player()
|
|
if not active_player:
|
|
return 0
|
|
position = active_player.playback_time
|
|
return position if position and position > 0 else 0
|
|
except:
|
|
return 0
|
|
|
|
def get_random_trailer(self):
|
|
"""Get random trailer video with anti-repeat logic"""
|
|
if not self.trailer_videos:
|
|
return None
|
|
|
|
# If we have very few trailers, just pick randomly
|
|
if len(self.trailer_videos) <= 3:
|
|
trailer = random.choice(self.trailer_videos)
|
|
self.logger.debug(f"Selected trailer (few available): {Path(trailer).name}")
|
|
return trailer
|
|
|
|
# Get trailers that haven't been played recently
|
|
available_trailers = [
|
|
t for t in self.trailer_videos
|
|
if t not in self.recently_played_trailers[-self.max_recent_trailers:]
|
|
]
|
|
|
|
# If all trailers were played recently, use the oldest ones
|
|
if not available_trailers:
|
|
self.logger.debug("All trailers recently played - clearing recent history")
|
|
# Keep only the most recent 5 to avoid immediate repeats
|
|
self.recently_played_trailers = self.recently_played_trailers[-5:]
|
|
available_trailers = [
|
|
t for t in self.trailer_videos
|
|
if t not in self.recently_played_trailers
|
|
]
|
|
|
|
# If still none available, just use all
|
|
if not available_trailers:
|
|
available_trailers = self.trailer_videos
|
|
|
|
# Select random trailer from available pool
|
|
trailer = random.choice(available_trailers)
|
|
|
|
# Add to history
|
|
self.recently_played_trailers.append(trailer)
|
|
|
|
# Trim history to max size
|
|
if len(self.recently_played_trailers) > self.trailer_history_size:
|
|
self.recently_played_trailers = self.recently_played_trailers[-self.trailer_history_size:]
|
|
|
|
recent_count = len([t for t in self.recently_played_trailers[-self.max_recent_trailers:] if t == trailer])
|
|
self.logger.info(f"Selected trailer: {Path(trailer).name}")
|
|
self.logger.debug(f"Available pool: {len(available_trailers)}/{len(self.trailer_videos)} trailers")
|
|
self.logger.debug(f"Recent history size: {len(self.recently_played_trailers)}")
|
|
|
|
return trailer
|
|
|
|
def process_video_queue(self):
|
|
"""Process video queue"""
|
|
processed = 0
|
|
|
|
try:
|
|
while not self.video_queue.empty():
|
|
try:
|
|
mode, video_path = self.video_queue.get_nowait()
|
|
processed += 1
|
|
|
|
video_name = Path(video_path).name
|
|
self.logger.info(f"Processing queued video: {video_name}")
|
|
|
|
is_specific = (mode == "specific")
|
|
|
|
if is_specific:
|
|
self.pending_specific_video = True
|
|
|
|
if self.seamless_transition(video_path, is_specific=is_specific):
|
|
self.current_mode = mode
|
|
self.stats['queue_processed'] += 1
|
|
|
|
if is_specific:
|
|
time.sleep(0.3)
|
|
duration = self.get_video_duration()
|
|
self.logger.info(f"Video duration: {duration:.1f}s")
|
|
self.pending_specific_video = False
|
|
|
|
# Clear remaining queue
|
|
while not self.video_queue.empty():
|
|
try:
|
|
self.video_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
break
|
|
else:
|
|
if is_specific:
|
|
self.pending_specific_video = False
|
|
|
|
except queue.Empty:
|
|
break
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Queue processing error: {e}", exc_info=True)
|
|
self.pending_specific_video = False
|
|
|
|
if processed > 0:
|
|
self.debug_console.update_queue_depth(self.video_queue.qsize())
|
|
|
|
def play_specific_video(self, nfc_id):
|
|
"""Handle NFC input - Enhanced with folder support"""
|
|
with self.lock:
|
|
try:
|
|
self.logger.info(f"Processing NFC input: {nfc_id}")
|
|
|
|
# Check if this is a folder mapping first
|
|
if nfc_id in self.folder_mapping:
|
|
self.logger.info(f"NFC {nfc_id} mapped to folder sequence")
|
|
|
|
video_path = self.get_next_video_from_folder(nfc_id)
|
|
|
|
if not video_path:
|
|
self.logger.error(f"Could not get video from folder for NFC {nfc_id}")
|
|
self.debug_console.log_error(f"No video available for NFC {nfc_id}")
|
|
return
|
|
|
|
if not Path(video_path).exists():
|
|
self.logger.error(f"Video file not found: {video_path}")
|
|
self.debug_console.log_error(f"File missing: {video_path}")
|
|
return
|
|
|
|
# Stop current video
|
|
try:
|
|
active_player = self.get_active_player()
|
|
if active_player:
|
|
active_player.stop()
|
|
except:
|
|
pass
|
|
|
|
# Clear queue
|
|
while not self.video_queue.empty():
|
|
try:
|
|
self.video_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
# Queue the folder video
|
|
self.video_queue.put(("specific", video_path))
|
|
self.logger.info(f"Queued folder video: {Path(video_path).name}")
|
|
self.pending_specific_video = True
|
|
|
|
self.debug_console.update_queue_depth(self.video_queue.qsize())
|
|
return
|
|
|
|
# Check single file mapping
|
|
if nfc_id in self.key_mapping:
|
|
self.logger.info(f"NFC {nfc_id} mapped to single file")
|
|
|
|
video_path = self.key_mapping[nfc_id]
|
|
|
|
if video_path.startswith("Missing:"):
|
|
self.logger.error(f"Video marked as missing for NFC {nfc_id}")
|
|
self.debug_console.log_error(f"Missing video for NFC {nfc_id}")
|
|
return
|
|
|
|
if not Path(video_path).exists():
|
|
self.logger.error(f"Video file not found: {video_path}")
|
|
self.debug_console.log_error(f"File missing: {video_path}")
|
|
return
|
|
|
|
# Stop current video
|
|
try:
|
|
active_player = self.get_active_player()
|
|
if active_player:
|
|
active_player.stop()
|
|
except:
|
|
pass
|
|
|
|
# Clear queue
|
|
while not self.video_queue.empty():
|
|
try:
|
|
self.video_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
# Queue the video
|
|
self.video_queue.put(("specific", video_path))
|
|
self.logger.info(f"Queued single file video: {Path(video_path).name}")
|
|
self.pending_specific_video = True
|
|
|
|
self.debug_console.update_queue_depth(self.video_queue.qsize())
|
|
return
|
|
|
|
# No mapping found
|
|
self.logger.warning(f"No mapping found for NFC ID: {nfc_id}")
|
|
self.debug_console.log(f"Unknown NFC ID: {nfc_id}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"NFC processing error: {e}", exc_info=True)
|
|
|
|
def run(self):
|
|
"""Main video player loop"""
|
|
self.running = True
|
|
self.start_time = time.time()
|
|
self.logger.info("=== MPV VIDEO PLAYER MAIN LOOP START ===")
|
|
|
|
# Start with random trailer
|
|
initial_trailer = self.get_random_trailer()
|
|
if initial_trailer:
|
|
self.seamless_transition(initial_trailer)
|
|
else:
|
|
self.logger.error("No initial trailer available!")
|
|
|
|
while self.running:
|
|
try:
|
|
# Process video queue
|
|
if not self.video_queue.empty():
|
|
self.process_video_queue()
|
|
|
|
# Force next video
|
|
elif self.force_next and not self.pending_specific_video:
|
|
self.force_next = False
|
|
|
|
if self.video_queue.empty():
|
|
if self.current_mode == "specific" or self.specific_video_playing:
|
|
self.current_mode = "trailers"
|
|
self.specific_video_playing = False
|
|
self.specific_video_start_time = None
|
|
|
|
next_trailer = self.get_random_trailer()
|
|
if next_trailer:
|
|
self.seamless_transition(next_trailer)
|
|
|
|
# Handle natural video end
|
|
elif not self.is_video_playing() and self.current_video_path:
|
|
self.logger.info("Video ended naturally")
|
|
|
|
if self.specific_video_playing or self.current_mode == "specific":
|
|
self.current_mode = "trailers"
|
|
self.specific_video_playing = False
|
|
self.specific_video_start_time = None
|
|
|
|
next_trailer = self.get_random_trailer()
|
|
if next_trailer:
|
|
time.sleep(self.config.TRANSITION_DELAY)
|
|
self.seamless_transition(next_trailer)
|
|
|
|
time.sleep(0.1)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Main loop error: {e}", exc_info=True)
|
|
time.sleep(1)
|
|
|
|
self.logger.info("=== MPV VIDEO PLAYER MAIN LOOP END ===")
|
|
|
|
def stop(self):
|
|
"""Clean up MPV resources"""
|
|
self.logger.info("=== MPV VIDEO PLAYER STOP ===")
|
|
self.running = False
|
|
|
|
try:
|
|
if self.player_a:
|
|
try:
|
|
self.player_a.stop()
|
|
self.player_a.terminate()
|
|
except:
|
|
pass
|
|
|
|
if self.player_b:
|
|
try:
|
|
self.player_b.stop()
|
|
self.player_b.terminate()
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Cleanup error: {e}")
|
|
|
|
self.logger.info("=== MPV CLEANUP COMPLETE ===") |