changes
This commit is contained in:
220
video_player.py
220
video_player.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user