diff --git a/CHANNEL_CONTROL.md b/CHANNEL_CONTROL.md new file mode 100644 index 0000000..747a488 --- /dev/null +++ b/CHANNEL_CONTROL.md @@ -0,0 +1,70 @@ +# Channel Control Commands + +This document describes the command-line tools for controlling the video player service. + +## Quick Usage + +### Simple Channel Change +```bash +# Change to random channel +./channel + +# Change to specific channel +./channel 5 +``` + +### Advanced Control +```bash +# Change to random channel +python3 change_channel.py + +# Change to specific channel +python3 change_channel.py 5 + +# Show current channel +python3 change_channel.py --current + +# List all available channels +python3 change_channel.py --list + +# Stop video player service +python3 change_channel.py --stop +``` + +## Features + +- **Random Channel Selection**: If no channel is specified, automatically selects a random channel +- **Service Communication**: Communicates with the running video player service via Unix socket +- **Real-time Control**: Changes channels instantly without restarting the service +- **Channel Information**: Shows current channel and available channels + +## Requirements + +- Video player service must be running +- Control socket must be available at `/tmp/video_player_control.sock` +- Python virtual environment activated (on server) + +## Examples + +```bash +# On the server (tulivision@192.168.1.160) +cd rpi-tulivision +source venv/bin/activate + +# Quick random channel change +./channel + +# Change to channel 7 +./channel 7 + +# Check what's currently playing +python3 change_channel.py --current +``` + +## Technical Details + +The channel control system uses: +- Unix domain sockets for inter-process communication +- JSON-based command protocol +- Thread-safe command processing +- Automatic error handling and timeouts diff --git a/change_channel.py b/change_channel.py new file mode 100755 index 0000000..347e9c4 --- /dev/null +++ b/change_channel.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +""" +Channel Change Command Line Tool +Allows changing channels in the running video player service +""" + +import os +import sys +import json +import argparse +import socket +import time +import signal +from pathlib import Path + +class ChannelController: + """Controller for changing channels in the video player service""" + + def __init__(self, socket_path="/tmp/video_player_control.sock"): + self.socket_path = socket_path + self.config_path = "config.json" + self.config = self.load_config() + + def load_config(self): + """Load configuration from JSON file""" + try: + if os.path.exists(self.config_path): + with open(self.config_path, 'r') as f: + return json.load(f) + else: + return {} + except Exception as e: + print(f"Error loading config: {e}") + return {} + + def send_command(self, command): + """Send command to video player service via Unix socket""" + try: + # Create Unix socket connection + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(5.0) # 5 second timeout + + # Connect to the video player service + sock.connect(self.socket_path) + + # Send command + command_data = json.dumps(command).encode('utf-8') + sock.send(command_data) + + # Receive response + response_data = sock.recv(1024).decode('utf-8') + response = json.loads(response_data) + + sock.close() + return response + + except FileNotFoundError: + print("Error: Video player service is not running or control socket not found") + print("Make sure the video player service is started") + return {"success": False, "error": "Service not running"} + except ConnectionRefusedError: + print("Error: Cannot connect to video player service") + print("The service may not be running or may not support control commands") + return {"success": False, "error": "Connection refused"} + except Exception as e: + print(f"Error communicating with video player service: {e}") + return {"success": False, "error": str(e)} + + def change_channel(self, channel_number=None): + """Change to specified channel or random if not specified""" + if channel_number is None: + command = {"action": "random_channel"} + print("Changing to random channel...") + else: + command = {"action": "change_channel", "channel": channel_number} + print(f"Changing to channel {channel_number}...") + + response = self.send_command(command) + + if response.get("success"): + if channel_number is None: + actual_channel = response.get("channel") + channel_name = response.get("channel_name", "Unknown") + print(f"Successfully changed to random channel {actual_channel}: {channel_name}") + else: + channel_name = response.get("channel_name", "Unknown") + print(f"Successfully changed to channel {channel_number}: {channel_name}") + else: + error = response.get("error", "Unknown error") + print(f"Failed to change channel: {error}") + return False + + return True + + def list_channels(self): + """List available channels""" + command = {"action": "list_channels"} + response = self.send_command(command) + + if response.get("success"): + channels = response.get("channels", {}) + if channels: + print("Available channels:") + print("-" * 50) + for channel_num in sorted(channels.keys()): + channel = channels[channel_num] + print(f"Channel {channel_num}: {channel['name']}") + else: + print("No channels available") + else: + error = response.get("error", "Unknown error") + print(f"Failed to list channels: {error}") + + def get_current_channel(self): + """Get current playing channel""" + command = {"action": "get_current_channel"} + response = self.send_command(command) + + if response.get("success"): + channel = response.get("channel") + channel_name = response.get("channel_name", "Unknown") + if channel: + print(f"Currently playing: Channel {channel} - {channel_name}") + else: + print("No channel currently playing") + else: + error = response.get("error", "Unknown error") + print(f"Failed to get current channel: {error}") + + def stop_service(self): + """Stop the video player service""" + command = {"action": "stop"} + response = self.send_command(command) + + if response.get("success"): + print("Video player service stopped") + else: + error = response.get("error", "Unknown error") + print(f"Failed to stop service: {error}") + +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description="Change channels in the video player service", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python3 change_channel.py # Change to random channel + python3 change_channel.py 5 # Change to channel 5 + python3 change_channel.py --list # List available channels + python3 change_channel.py --current # Show current channel + python3 change_channel.py --stop # Stop video player service + """ + ) + + parser.add_argument( + 'channel', + nargs='?', + type=int, + help='Channel number to change to (if not specified, changes to random channel)' + ) + + parser.add_argument( + '--list', + action='store_true', + help='List available channels' + ) + + parser.add_argument( + '--current', + action='store_true', + help='Show current playing channel' + ) + + parser.add_argument( + '--stop', + action='store_true', + help='Stop the video player service' + ) + + parser.add_argument( + '--socket', + type=str, + default='/tmp/video_player_control.sock', + help='Path to control socket (default: /tmp/video_player_control.sock)' + ) + + return parser.parse_args() + +def main(): + """Main entry point""" + args = parse_arguments() + + controller = ChannelController(args.socket) + + if args.list: + controller.list_channels() + elif args.current: + controller.get_current_channel() + elif args.stop: + controller.stop_service() + else: + # Change channel (random if not specified) + controller.change_channel(args.channel) + +if __name__ == "__main__": + main() diff --git a/channel b/channel new file mode 100755 index 0000000..e75d556 --- /dev/null +++ b/channel @@ -0,0 +1,12 @@ +#!/bin/bash +# Simple wrapper script for changing channels +# Usage: ./channel [channel_number] or ./channel (for random) + +# Default to random if no argument provided +if [ $# -eq 0 ]; then + echo "Changing to random channel..." + python3 change_channel.py +else + echo "Changing to channel $1..." + python3 change_channel.py "$1" +fi diff --git a/video_player.py b/video_player.py index 7530913..aaa9192 100644 --- a/video_player.py +++ b/video_player.py @@ -14,6 +14,7 @@ import queue import subprocess import argparse import random +import socket from pathlib import Path from typing import Dict, List, Optional, Tuple import vlc @@ -60,6 +61,11 @@ class VideoPlayer: # Channel refresh tracking self.last_channel_refresh = time.time() + # Control socket for external commands + self.control_socket_path = "/tmp/video_player_control.sock" + self.control_socket = None + self.control_thread = None + self.logger.info("Video Player initialized") def load_config(self) -> Dict: @@ -143,6 +149,10 @@ class VideoPlayer: self.vlc_instance = vlc.Instance(all_vlc_options) self.vlc_player = self.vlc_instance.media_player_new() + # Set up event manager for video end detection + self.vlc_event_manager = self.vlc_player.event_manager() + self.vlc_event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, self.on_video_end) + # Set fullscreen and always on top if '--fullscreen' in vlc_options: self.vlc_player.set_fullscreen(True) @@ -472,6 +482,116 @@ class VideoPlayer: self.logger.info("Power toggle - shutting down") self.running = False + def setup_control_socket(self): + """Setup Unix socket for external control commands""" + try: + # Remove existing socket file if it exists + if os.path.exists(self.control_socket_path): + os.unlink(self.control_socket_path) + + # Create Unix socket + self.control_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.control_socket.bind(self.control_socket_path) + self.control_socket.listen(1) + + # Start control thread + self.control_thread = threading.Thread(target=self.handle_control_commands, daemon=True) + self.control_thread.start() + + self.logger.info(f"Control socket setup at {self.control_socket_path}") + return True + except Exception as e: + self.logger.error(f"Failed to setup control socket: {e}") + return False + + def handle_control_commands(self): + """Handle control commands from external clients""" + while self.running: + try: + # Accept connection + client_socket, _ = self.control_socket.accept() + + # Receive command + data = client_socket.recv(1024).decode('utf-8') + command = json.loads(data) + + # Process command + response = self.process_control_command(command) + + # Send response + response_data = json.dumps(response).encode('utf-8') + client_socket.send(response_data) + + client_socket.close() + + except Exception as e: + if self.running: # Only log if we're still supposed to be running + self.logger.error(f"Error handling control command: {e}") + time.sleep(0.1) + + def process_control_command(self, command): + """Process a control command and return response""" + action = command.get("action") + + try: + if action == "change_channel": + channel = command.get("channel") + if channel and channel in self.channels: + success = self.play_channel(channel) + if success: + return { + "success": True, + "channel": channel, + "channel_name": self.channels[channel]["name"] + } + else: + return {"success": False, "error": "Failed to play channel"} + else: + return {"success": False, "error": f"Invalid channel: {channel}"} + + elif action == "random_channel": + success = self.play_random_video() + if success: + return { + "success": True, + "channel": self.current_channel, + "channel_name": self.channels[self.current_channel]["name"] if self.current_channel else "Unknown" + } + else: + return {"success": False, "error": "Failed to play random channel"} + + elif action == "list_channels": + return { + "success": True, + "channels": self.channels + } + + elif action == "get_current_channel": + if self.current_channel and self.current_channel in self.channels: + return { + "success": True, + "channel": self.current_channel, + "channel_name": self.channels[self.current_channel]["name"] + } + else: + return { + "success": True, + "channel": None, + "channel_name": None + } + + elif action == "stop": + self.logger.info("Stop command received via control socket") + self.running = False + return {"success": True} + + else: + return {"success": False, "error": f"Unknown action: {action}"} + + except Exception as e: + self.logger.error(f"Error processing control command: {e}") + return {"success": False, "error": str(e)} + def start(self, startup_mode: str = "default", channel_number: int = None, video_index: int = None): """Start the video player with specified mode @@ -509,6 +629,10 @@ class VideoPlayer: else: self.logger.info("IR remote control disabled, skipping IR processing thread") + # Setup control socket for external commands + if not self.setup_control_socket(): + self.logger.warning("Failed to setup control socket, external commands will not work") + # Play based on startup mode success = False if startup_mode == "random": @@ -566,17 +690,21 @@ class VideoPlayer: finally: self.cleanup() + def on_video_end(self, event): + """Callback for when a video ends - start a random channel""" + self.logger.info("Video ended, starting random channel") + if self.channels: + self.play_random_video() + else: + self.logger.error("No channels available to play after video end") + def ensure_video_playing(self): """Ensure a video is always playing""" if not self.vlc_player or not self.vlc_player.is_playing(): - if self.current_channel and self.current_channel in self.channels: - self.logger.info(f"Restarting current channel {self.current_channel}") - self.play_channel(self.current_channel) - elif self.channels: - # Play the first available channel - first_channel = min(self.channels.keys()) - self.logger.info(f"No current channel, playing first available channel {first_channel}") - self.play_channel(first_channel) + if self.channels: + # Play a random channel instead of restarting current or first channel + self.logger.info("No video playing, starting random channel") + self.play_random_video() else: self.logger.error("No channels available to play") @@ -600,6 +728,21 @@ class VideoPlayer: if self.enable_ir: GPIO.cleanup() + + # Cleanup control socket + if self.control_socket: + try: + self.control_socket.close() + except: + pass + + # Remove socket file + if os.path.exists(self.control_socket_path): + try: + os.unlink(self.control_socket_path) + except: + pass + self.logger.info("Cleanup complete") def parse_arguments():