This commit is contained in:
2025-09-25 18:17:12 +02:00
parent 8de07d073d
commit 0ee635bd2a
8 changed files with 361 additions and 146 deletions

View File

@@ -27,7 +27,7 @@ load_dotenv()
class VideoPlayer:
"""Main video player class with VLC integration and IR remote control"""
def __init__(self, config_path: str = "config.json"):
def __init__(self, config_path: str = "config.json", enable_ir: bool = True):
"""Initialize the video player with configuration"""
self.config_path = config_path
self.config = self.load_config()
@@ -40,6 +40,7 @@ class VideoPlayer:
self.current_channel = None
self.ir_queue = queue.Queue()
self.running = False
self.enable_ir = enable_ir
# IR Remote settings
self.ir_pin = self.config.get('ir_pin', 18)
@@ -50,11 +51,15 @@ class VideoPlayer:
self.default_channel = self.config.get('default_channel', 1)
self.channel_timeout = self.config.get('channel_timeout', 3.0)
self.multi_digit_timeout = self.config.get('multi_digit_timeout', 1.0)
self.channel_refresh_interval = self.config.get('channel_refresh_interval', 30.0) # seconds
# Multi-digit channel input
self.channel_input = ""
self.last_digit_time = 0
# Channel refresh tracking
self.last_channel_refresh = time.time()
self.logger.info("Video Player initialized")
def load_config(self) -> Dict:
@@ -78,34 +83,11 @@ class VideoPlayer:
"default_channel": 1,
"channel_timeout": 3.0,
"multi_digit_timeout": 1.0,
"channel_refresh_interval": 30.0,
"vlc_options": [
"--fullscreen",
"--no-video-title-show",
"--no-audio-display",
"--always-on-top",
"--video-on-top",
"--no-embedded-video",
"--no-video-deco",
"--no-qt-fs-controller",
"--no-qt-system-tray",
"--no-qt-notification",
"--no-qt-privacy-ask",
"--no-qt-updates-notif",
"--no-qt-error-dialogs",
"--no-qt-fs-controller",
"--no-qt-video-autosize",
"--no-qt-name-in-title",
"--no-qt-minimal-view",
"--no-qt-bgcone",
"--no-qt-pause-minimized",
"--no-qt-continue",
"--no-qt-recentplay",
"--no-qt-start-minimized",
"--no-qt-system-tray",
"--no-qt-notification",
"--no-qt-privacy-ask",
"--no-qt-updates-notif",
"--no-qt-error-dialogs"
"--quiet"
],
"ir_codes": {
"0": "channel_0",
@@ -154,7 +136,11 @@ class VideoPlayer:
"""Initialize VLC media player"""
try:
vlc_options = self.config.get('vlc_options', [])
self.vlc_instance = vlc.Instance(vlc_options)
# Use the configured VLC options
all_vlc_options = vlc_options
self.vlc_instance = vlc.Instance(all_vlc_options)
self.vlc_player = self.vlc_instance.media_player_new()
# Set fullscreen and always on top
@@ -201,7 +187,7 @@ class VideoPlayer:
return video_files
def create_channels(self):
"""Create channel mapping from video files"""
"""Create channel mapping from video files automatically"""
video_files = self.scan_video_folder()
self.channels = {}
@@ -213,52 +199,40 @@ class VideoPlayer:
'file': video_file
}
self.logger.info(f"Created {len(self.channels)} channels")
# Save channel mapping to file
self.save_channel_mapping()
self.logger.info(f"Created {len(self.channels)} channels automatically from directory")
def save_channel_mapping(self):
"""Save channel mapping to JSON file"""
def load_channels_from_directory(self):
"""Load channels automatically from video directory"""
try:
channel_data = {}
for channel_num, channel_info in self.channels.items():
channel_data[str(channel_num)] = {
'name': channel_info['name'],
'path': channel_info['path']
}
with open('channels.json', 'w') as f:
json.dump(channel_data, f, indent=2)
self.logger.info("Channel mapping saved to channels.json")
except Exception as e:
self.logger.error(f"Error saving channel mapping: {e}")
def load_channel_mapping(self):
"""Load channel mapping from JSON file"""
try:
if os.path.exists('channels.json'):
with open('channels.json', 'r') as f:
channel_data = json.load(f)
self.channels = {}
for channel_num, channel_info in channel_data.items():
video_path = Path(channel_info['path'])
if video_path.exists():
self.channels[int(channel_num)] = {
'number': int(channel_num),
'name': channel_info['name'],
'path': channel_info['path'],
'file': video_path
}
self.logger.info(f"Loaded {len(self.channels)} channels from mapping file")
self.create_channels()
if self.channels:
self.logger.info(f"Loaded {len(self.channels)} channels from directory")
return True
else:
self.logger.warning("No video files found in directory")
return False
except Exception as e:
self.logger.error(f"Error loading channel mapping: {e}")
self.logger.error(f"Error loading channels from directory: {e}")
return False
def refresh_channels(self):
"""Refresh channel list from directory (useful when videos are added/removed)"""
old_channel_count = len(self.channels)
self.load_channels_from_directory()
new_channel_count = len(self.channels)
return False
if new_channel_count != old_channel_count:
self.logger.info(f"Channel count changed: {old_channel_count} -> {new_channel_count}")
# If current channel no longer exists, switch to first available
if self.current_channel and self.current_channel not in self.channels:
if self.channels:
first_channel = min(self.channels.keys())
self.logger.info(f"Current channel {self.current_channel} no longer exists, switching to channel {first_channel}")
self.play_channel(first_channel)
else:
self.logger.warning("No channels available after refresh")
self.current_channel = None
def play_channel(self, channel_number: int) -> bool:
"""Play video for specified channel number"""
@@ -327,6 +301,10 @@ class VideoPlayer:
def setup_gpio(self):
"""Setup GPIO for IR receiver"""
if not self.enable_ir:
self.logger.info("IR remote control disabled, skipping GPIO setup")
return True
try:
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.ir_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
@@ -515,17 +493,21 @@ class VideoPlayer:
self.logger.error("Failed to setup GPIO")
return False
# Load or create channels
if not self.load_channel_mapping():
self.create_channels()
# Load channels automatically from directory
if not self.load_channels_from_directory():
self.logger.error("Failed to load channels from directory")
return False
if not self.channels:
self.logger.error("No video files found")
return False
# Start IR processing thread
ir_thread = threading.Thread(target=self.process_ir_commands, daemon=True)
ir_thread.start()
# Start IR processing thread only if IR is enabled
if self.enable_ir:
ir_thread = threading.Thread(target=self.process_ir_commands, daemon=True)
ir_thread.start()
else:
self.logger.info("IR remote control disabled, skipping IR processing thread")
# Play based on startup mode
success = False
@@ -566,6 +548,12 @@ class VideoPlayer:
self.logger.info("Video stopped, ensuring video is playing")
self.ensure_video_playing()
# Periodically refresh channels to detect new/removed videos
current_time = time.time()
if current_time - self.last_channel_refresh > self.channel_refresh_interval:
self.refresh_channels()
self.last_channel_refresh = current_time
# Keep VLC on top
self.keep_vlc_on_top()
@@ -610,7 +598,8 @@ class VideoPlayer:
if self.vlc_player:
self.vlc_player.stop()
GPIO.cleanup()
if self.enable_ir:
GPIO.cleanup()
self.logger.info("Cleanup complete")
def parse_arguments():
@@ -620,12 +609,18 @@ def parse_arguments():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 video_player.py # Start with default channel
python3 video_player.py # Start with default channel (auto-assigned)
python3 video_player.py --random # Start with random video
python3 video_player.py --channel 5 # Start with channel 5
python3 video_player.py --channel 5 # Start with channel 5 (auto-assigned)
python3 video_player.py --index 2 # Start with video at index 2 (0-based)
python3 video_player.py --list-channels # List available channels
python3 video_player.py --no-ir # Start without IR remote control
python3 video_player.py --list-channels # List available channels (auto-assigned)
python3 video_player.py --list-videos # List available videos with indices
python3 video_player.py --refresh-channels # Refresh channel list from directory
Note: Channels are automatically assigned numbers (1, 2, 3...) based on alphabetical order
of video files found in the video directory. No channel mapping file is needed.
Use --no-ir to start without IR remote control if GPIO access is not available.
"""
)
@@ -660,6 +655,11 @@ Examples:
action='store_true',
help='List available videos with indices and exit'
)
parser.add_argument(
'--refresh-channels',
action='store_true',
help='Refresh channel list from directory and exit'
)
# Configuration arguments
parser.add_argument(
@@ -668,6 +668,11 @@ Examples:
default='config.json',
help='Path to configuration file (default: config.json)'
)
parser.add_argument(
'--no-ir',
action='store_true',
help='Start without IR remote control (skip GPIO setup)'
)
return parser.parse_args()
@@ -704,37 +709,48 @@ def main():
"""Main entry point"""
args = parse_arguments()
# Check if user is in gpio group (needed for GPIO access)
import grp
try:
gpio_group = grp.getgrnam('gpio')
current_groups = os.getgroups()
if gpio_group.gr_gid not in current_groups:
print("This script requires GPIO access. Please add your user to the gpio group:")
print("sudo usermod -a -G gpio $USER")
print("Then log out and log back in, or run: newgrp gpio")
# Check if user is in gpio group (needed for GPIO access) only if IR is enabled
if not args.no_ir:
import grp
try:
gpio_group = grp.getgrnam('gpio')
current_groups = os.getgroups()
if gpio_group.gr_gid not in current_groups:
print("This script requires GPIO access. Please add your user to the gpio group:")
print("sudo usermod -a -G gpio $USER")
print("Then log out and log back in, or run: newgrp gpio")
print("Alternatively, use --no-ir to start without IR remote control")
sys.exit(1)
except KeyError:
print("GPIO group not found. Please ensure your system has GPIO support.")
print("Alternatively, use --no-ir to start without IR remote control")
sys.exit(1)
except KeyError:
print("GPIO group not found. Please ensure your system has GPIO support.")
sys.exit(1)
# Check if another instance is running
current_pid = os.getpid()
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
if 'video_player.py' in ' '.join(proc.info['cmdline'] or []) and proc.info['pid'] != os.getpid():
print("Another instance of video_player.py is already running")
sys.exit(1)
if proc.info['cmdline']:
cmdline = proc.info['cmdline']
# Check if this is actually running video_player.py (not just containing it in args)
if (len(cmdline) > 1 and
cmdline[1].endswith('video_player.py') and
proc.info['pid'] != current_pid):
print("Another instance of video_player.py is already running")
sys.exit(1)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
# Create video player
player = VideoPlayer(args.config)
enable_ir = not args.no_ir
player = VideoPlayer(args.config, enable_ir=enable_ir)
# Handle list commands
if args.list_channels:
# Load channels first
if not player.load_channel_mapping():
player.create_channels()
# Load channels from directory
if not player.load_channels_from_directory():
print("No video files found in directory")
return
list_channels(player)
return
@@ -742,6 +758,14 @@ def main():
list_videos(player)
return
if args.refresh_channels:
if player.load_channels_from_directory():
print(f"Refreshed channels: {len(player.channels)} channels found")
list_channels(player)
else:
print("No video files found in directory")
return
# Determine startup mode and parameters
startup_mode = "default"
channel_number = None