diff --git a/video-player-random.service b/video-player-random.service new file mode 100644 index 0000000..3fc99fa --- /dev/null +++ b/video-player-random.service @@ -0,0 +1,39 @@ +[Unit] +Description=Raspberry Pi Video Player with Random Video Startup +Documentation=https://github.com/your-repo/ulivision-tv +After=network.target sound.target graphical-session.target +Wants=graphical-session.target + +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=/opt/video_player +ExecStart=/opt/video_player/venv/bin/python /opt/video_player/video_player.py --random +ExecReload=/bin/kill -HUP $MAINPID +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=video-player-random + +# Environment variables +Environment=DISPLAY=:0 +Environment=XAUTHORITY=/home/pi/.Xauthority +Environment=PYTHONPATH=/opt/video_player + +# Security settings +NoNewPrivileges=false +PrivateTmp=false +ProtectSystem=false +ProtectHome=false + +# Resource limits +LimitNOFILE=65536 +MemoryMax=512M + +# GPIO access +SupplementaryGroups=gpio + +[Install] +WantedBy=multi-user.target diff --git a/video_control.py b/video_control.py new file mode 100755 index 0000000..e472c35 --- /dev/null +++ b/video_control.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +Video Control Script +Simple command-line interface for controlling the video player +""" + +import os +import sys +import argparse +import subprocess +from pathlib import Path + +def run_video_player(args): + """Run the video player with specified arguments""" + script_dir = Path(__file__).parent + video_player_script = script_dir / "video_player.py" + + if not video_player_script.exists(): + print(f"Error: video_player.py not found at {video_player_script}") + return False + + # Build command + cmd = ["sudo", "python3", str(video_player_script)] + + if args.random: + cmd.append("--random") + elif args.channel is not None: + cmd.extend(["--channel", str(args.channel)]) + elif args.index is not None: + cmd.extend(["--index", str(args.index)]) + elif args.list_channels: + cmd.append("--list-channels") + elif args.list_videos: + cmd.append("--list-videos") + + if args.config: + cmd.extend(["--config", args.config]) + + try: + print(f"Running: {' '.join(cmd)}") + subprocess.run(cmd, check=True) + return True + except subprocess.CalledProcessError as e: + print(f"Error running video player: {e}") + return False + except KeyboardInterrupt: + print("\nInterrupted by user") + return False + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Video Control - Simple interface for video player", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python3 video_control.py --random # Play random video + python3 video_control.py --channel 5 # Play channel 5 + python3 video_control.py --index 2 # Play video at index 2 + python3 video_control.py --list-channels # List channels + python3 video_control.py --list-videos # List videos + """ + ) + + # Mode arguments (mutually exclusive) + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument( + '--random', + action='store_true', + help='Play a random video' + ) + mode_group.add_argument( + '--channel', + type=int, + metavar='N', + help='Play specific channel number' + ) + mode_group.add_argument( + '--index', + type=int, + metavar='N', + help='Play video at specific index (0-based)' + ) + mode_group.add_argument( + '--list-channels', + action='store_true', + help='List available channels and exit' + ) + mode_group.add_argument( + '--list-videos', + action='store_true', + help='List available videos with indices and exit' + ) + + # Configuration arguments + parser.add_argument( + '--config', + type=str, + help='Path to configuration file' + ) + + args = parser.parse_args() + + # Check if at least one mode is specified + if not any([args.random, args.channel is not None, args.index is not None, + args.list_channels, args.list_videos]): + parser.print_help() + return + + # Run video player + success = run_video_player(args) + sys.exit(0 if success else 1) + +if __name__ == "__main__": + main() diff --git a/video_player.py b/video_player.py index c51f393..268a3df 100644 --- a/video_player.py +++ b/video_player.py @@ -12,6 +12,8 @@ import logging import threading import queue import subprocess +import argparse +import random from pathlib import Path from typing import Dict, List, Optional, Tuple import vlc @@ -244,6 +246,49 @@ class VideoPlayer: self.logger.error(f"Error playing channel {channel_number}: {e}") return False + def play_random_video(self) -> bool: + """Play a random video from the available channels""" + if not self.channels: + self.logger.error("No channels available for random playback") + return False + + try: + # Get a random channel number + random_channel = random.choice(list(self.channels.keys())) + self.logger.info(f"Selected random channel: {random_channel}") + return self.play_channel(random_channel) + except Exception as e: + self.logger.error(f"Error playing random video: {e}") + return False + + def play_video_by_index(self, index: int) -> bool: + """Play video by folder index (0-based)""" + video_files = self.scan_video_folder() + + if not video_files: + self.logger.error("No video files found") + return False + + if index < 0 or index >= len(video_files): + self.logger.warning(f"Index {index} out of range (0-{len(video_files)-1})") + return False + + try: + video_file = video_files[index] + media = self.vlc_instance.media_new(str(video_file)) + self.vlc_player.set_media(media) + self.vlc_player.play() + + self.current_channel = None # Not a channel-based playback + self.logger.info(f"Playing video by index {index}: {video_file.name}") + + # Wait for media to start playing + time.sleep(0.5) + return True + except Exception as e: + self.logger.error(f"Error playing video by index {index}: {e}") + return False + def setup_gpio(self): """Setup GPIO for IR receiver""" try: @@ -413,9 +458,15 @@ class VideoPlayer: self.logger.info("Power toggle - shutting down") self.running = False - def start(self): - """Start the video player""" - self.logger.info("Starting video player...") + def start(self, startup_mode: str = "default", channel_number: int = None, video_index: int = None): + """Start the video player with specified mode + + Args: + startup_mode: "default", "random", "channel", or "index" + channel_number: Channel number to play (for channel mode) + video_index: Video index to play (for index mode) + """ + self.logger.info(f"Starting video player in {startup_mode} mode...") self.running = True # Initialize VLC @@ -440,20 +491,32 @@ class VideoPlayer: ir_thread = threading.Thread(target=self.process_ir_commands, daemon=True) ir_thread.start() - # Play default channel - if self.default_channel in self.channels: - self.play_channel(self.default_channel) - else: - # Play first available channel - first_channel = min(self.channels.keys()) - self.play_channel(first_channel) + # Play based on startup mode + success = False + if startup_mode == "random": + success = self.play_random_video() + elif startup_mode == "channel" and channel_number is not None: + success = self.play_channel(channel_number) + elif startup_mode == "index" and video_index is not None: + success = self.play_video_by_index(video_index) + else: # default mode + if self.default_channel in self.channels: + success = self.play_channel(self.default_channel) + else: + # Play first available channel + first_channel = min(self.channels.keys()) + success = self.play_channel(first_channel) - self.logger.info("Video player started successfully") - return True + if success: + self.logger.info("Video player started successfully") + else: + self.logger.error("Failed to start video playback") + + return success - def run(self): + def run(self, startup_mode: str = "default", channel_number: int = None, video_index: int = None): """Main run loop""" - if not self.start(): + if not self.start(startup_mode, channel_number, video_index): return try: @@ -485,8 +548,97 @@ class VideoPlayer: GPIO.cleanup() self.logger.info("Cleanup complete") +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description="Raspberry Pi Video Player with IR Remote Control", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python3 video_player.py # Start with default channel + python3 video_player.py --random # Start with random video + python3 video_player.py --channel 5 # Start with channel 5 + 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 --list-videos # List available videos with indices + """ + ) + + # Startup mode arguments (mutually exclusive) + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument( + '--random', + action='store_true', + help='Start with a random video' + ) + mode_group.add_argument( + '--channel', + type=int, + metavar='N', + help='Start with specific channel number' + ) + mode_group.add_argument( + '--index', + type=int, + metavar='N', + help='Start with video at specific index (0-based)' + ) + + # Information arguments + parser.add_argument( + '--list-channels', + action='store_true', + help='List available channels and exit' + ) + parser.add_argument( + '--list-videos', + action='store_true', + help='List available videos with indices and exit' + ) + + # Configuration arguments + parser.add_argument( + '--config', + type=str, + default='config.json', + help='Path to configuration file (default: config.json)' + ) + + return parser.parse_args() + +def list_channels(player): + """List available channels""" + if not player.channels: + print("No channels available") + return + + print("Available channels:") + print("-" * 50) + for channel_num in sorted(player.channels.keys()): + channel = player.channels[channel_num] + print(f"Channel {channel_num}: {channel['name']}") + print(f" Path: {channel['path']}") + print() + +def list_videos(player): + """List available videos with indices""" + video_files = player.scan_video_folder() + + if not video_files: + print("No video files found") + return + + print("Available videos (with indices):") + print("-" * 50) + for i, video_file in enumerate(video_files): + print(f"Index {i}: {video_file.name}") + print(f" Path: {video_file}") + print() + def main(): """Main entry point""" + args = parse_arguments() + # Check if running as root (needed for GPIO access) if os.geteuid() != 0: print("This script must be run as root for GPIO access") @@ -502,9 +654,37 @@ def main(): except (psutil.NoSuchProcess, psutil.AccessDenied): continue - # Create and run video player - player = VideoPlayer() - player.run() + # Create video player + player = VideoPlayer(args.config) + + # Handle list commands + if args.list_channels: + # Load channels first + if not player.load_channel_mapping(): + player.create_channels() + list_channels(player) + return + + if args.list_videos: + list_videos(player) + return + + # Determine startup mode and parameters + startup_mode = "default" + channel_number = None + video_index = None + + if args.random: + startup_mode = "random" + elif args.channel is not None: + startup_mode = "channel" + channel_number = args.channel + elif args.index is not None: + startup_mode = "index" + video_index = args.index + + # Run video player + player.run(startup_mode, channel_number, video_index) if __name__ == "__main__": main()