channels
This commit is contained in:
70
CHANNEL_CONTROL.md
Normal file
70
CHANNEL_CONTROL.md
Normal 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
207
change_channel.py
Executable 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
12
channel
Executable 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
|
||||
159
video_player.py
159
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():
|
||||
|
||||
Reference in New Issue
Block a user