This commit is contained in:
2025-09-25 18:43:27 +02:00
parent 0ee635bd2a
commit f0f1818917
4 changed files with 440 additions and 8 deletions

70
CHANNEL_CONTROL.md Normal file
View File

@@ -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

207
change_channel.py Executable file
View File

@@ -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()

12
channel Executable file
View File

@@ -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

View File

@@ -14,6 +14,7 @@ import queue
import subprocess import subprocess
import argparse import argparse
import random import random
import socket
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
import vlc import vlc
@@ -60,6 +61,11 @@ class VideoPlayer:
# Channel refresh tracking # Channel refresh tracking
self.last_channel_refresh = time.time() 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") self.logger.info("Video Player initialized")
def load_config(self) -> Dict: def load_config(self) -> Dict:
@@ -143,6 +149,10 @@ class VideoPlayer:
self.vlc_instance = vlc.Instance(all_vlc_options) self.vlc_instance = vlc.Instance(all_vlc_options)
self.vlc_player = self.vlc_instance.media_player_new() 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 # Set fullscreen and always on top
if '--fullscreen' in vlc_options: if '--fullscreen' in vlc_options:
self.vlc_player.set_fullscreen(True) self.vlc_player.set_fullscreen(True)
@@ -472,6 +482,116 @@ class VideoPlayer:
self.logger.info("Power toggle - shutting down") self.logger.info("Power toggle - shutting down")
self.running = False 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): def start(self, startup_mode: str = "default", channel_number: int = None, video_index: int = None):
"""Start the video player with specified mode """Start the video player with specified mode
@@ -509,6 +629,10 @@ class VideoPlayer:
else: else:
self.logger.info("IR remote control disabled, skipping IR processing thread") 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 # Play based on startup mode
success = False success = False
if startup_mode == "random": if startup_mode == "random":
@@ -566,17 +690,21 @@ class VideoPlayer:
finally: finally:
self.cleanup() 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): def ensure_video_playing(self):
"""Ensure a video is always playing""" """Ensure a video is always playing"""
if not self.vlc_player or not self.vlc_player.is_playing(): if not self.vlc_player or not self.vlc_player.is_playing():
if self.current_channel and self.current_channel in self.channels: if self.channels:
self.logger.info(f"Restarting current channel {self.current_channel}") # Play a random channel instead of restarting current or first channel
self.play_channel(self.current_channel) self.logger.info("No video playing, starting random channel")
elif self.channels: self.play_random_video()
# 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)
else: else:
self.logger.error("No channels available to play") self.logger.error("No channels available to play")
@@ -600,6 +728,21 @@ class VideoPlayer:
if self.enable_ir: if self.enable_ir:
GPIO.cleanup() 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") self.logger.info("Cleanup complete")
def parse_arguments(): def parse_arguments():