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 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():
|
||||||
|
|||||||
Reference in New Issue
Block a user