Compare commits
2 Commits
8de07d073d
...
f0f1818917
| Author | SHA1 | Date | |
|---|---|---|---|
| f0f1818917 | |||
| 0ee635bd2a |
120
BOOT_SETUP.md
Normal file
120
BOOT_SETUP.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Video Player Boot Setup
|
||||
|
||||
This document describes how the video player has been configured to start automatically on boot without IR remote control.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Updated Service Files
|
||||
|
||||
Both service files have been updated with the following changes:
|
||||
|
||||
- **User**: Changed from `pi` to `tulivision`
|
||||
- **Working Directory**: Updated to `/home/tulivision/rpi-tulivision`
|
||||
- **ExecStart**: Updated to use virtual environment Python and added `--no-ir` flag
|
||||
- **Environment Variables**: Updated paths for tulivision user
|
||||
- **Removed**: GPIO group requirement (no longer needed with `--no-ir`)
|
||||
- **Added**: Better process termination handling
|
||||
|
||||
### 2. Service Files
|
||||
|
||||
#### video-player.service
|
||||
- **Description**: Raspberry Pi Video Player (No IR Remote Control)
|
||||
- **Command**: `video_player.py --no-ir`
|
||||
- **Behavior**: Starts with default channel (channel 1)
|
||||
|
||||
#### video-player-random.service
|
||||
- **Description**: Raspberry Pi Video Player with Random Video Startup (No IR Remote Control)
|
||||
- **Command**: `video_player.py --no-ir --random`
|
||||
- **Behavior**: Starts with a random video
|
||||
|
||||
### 3. Management Script
|
||||
|
||||
A management script `manage_video_player.sh` has been created with the following commands:
|
||||
|
||||
```bash
|
||||
./manage_video_player.sh start # Start the video player service
|
||||
./manage_video_player.sh stop # Stop the video player service
|
||||
./manage_video_player.sh restart # Restart the video player service
|
||||
./manage_video_player.sh status # Show service status
|
||||
./manage_video_player.sh enable # Enable video player for boot (default channel)
|
||||
./manage_video_player.sh enable-random # Enable random video player for boot
|
||||
./manage_video_player.sh disable # Disable video player services from boot
|
||||
./manage_video_player.sh logs # Show service logs (follow mode)
|
||||
```
|
||||
|
||||
## Installation Status
|
||||
|
||||
- ✅ Service files installed to `/etc/systemd/system/`
|
||||
- ✅ `video-player.service` enabled for boot
|
||||
- ✅ `video-player-random.service` available but disabled
|
||||
- ✅ Management script available in project directory
|
||||
|
||||
## Current Configuration
|
||||
|
||||
- **Active Service**: `video-player.service` (enabled for boot)
|
||||
- **Startup Mode**: Default channel (channel 1)
|
||||
- **IR Remote**: Disabled (no GPIO access required)
|
||||
- **User**: tulivision
|
||||
- **Working Directory**: /home/tulivision/rpi-tulivision
|
||||
- **Python Environment**: Virtual environment in venv/
|
||||
|
||||
## Testing Results
|
||||
|
||||
- ✅ Service starts successfully
|
||||
- ✅ VLC initializes without errors
|
||||
- ✅ Video playback works
|
||||
- ✅ Service stops cleanly
|
||||
- ✅ No GPIO access required
|
||||
- ✅ Automatic restart on failure
|
||||
|
||||
## Boot Behavior
|
||||
|
||||
When the system boots:
|
||||
|
||||
1. The `video-player.service` will start automatically
|
||||
2. It will initialize VLC without IR remote control
|
||||
3. It will scan the video directory and create channels automatically
|
||||
4. It will start playing the default channel (channel 1)
|
||||
5. If the service fails, it will restart automatically after 10 seconds
|
||||
|
||||
## Switching Between Services
|
||||
|
||||
To switch to random video startup:
|
||||
```bash
|
||||
./manage_video_player.sh enable-random
|
||||
```
|
||||
|
||||
To switch back to default channel startup:
|
||||
```bash
|
||||
./manage_video_player.sh enable
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Service Status
|
||||
```bash
|
||||
./manage_video_player.sh status
|
||||
```
|
||||
|
||||
### View Service Logs
|
||||
```bash
|
||||
./manage_video_player.sh logs
|
||||
```
|
||||
|
||||
### Restart Service
|
||||
```bash
|
||||
./manage_video_player.sh restart
|
||||
```
|
||||
|
||||
### Disable Auto-Start
|
||||
```bash
|
||||
./manage_video_player.sh disable
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The service runs without IR remote control, so no GPIO access is required
|
||||
- Videos are automatically discovered from the configured directory
|
||||
- Channels are assigned automatically based on alphabetical order
|
||||
- The service will restart automatically if it crashes
|
||||
- All logging is sent to systemd journal
|
||||
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
|
||||
@@ -41,6 +41,7 @@ class VideoPlayerConfig:
|
||||
channel_timeout: float = 3.0
|
||||
multi_digit_timeout: float = 1.0
|
||||
channel_display_timeout: float = 2.0
|
||||
channel_refresh_interval: float = 30.0 # seconds
|
||||
channel_assignment_method: str = "alphabetical" # alphabetical, manual, custom
|
||||
|
||||
# IR Remote settings
|
||||
@@ -435,6 +436,7 @@ def create_config_templates():
|
||||
"channel_timeout": 3.0,
|
||||
"multi_digit_timeout": 1.0,
|
||||
"channel_display_timeout": 2.0,
|
||||
"channel_refresh_interval": 30.0,
|
||||
"channel_assignment_method": "alphabetical",
|
||||
"ir_pin": 18,
|
||||
"ir_protocols": ["NEC", "RC5"],
|
||||
|
||||
63
manage_video_player.sh
Executable file
63
manage_video_player.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Video Player Service Management Script
|
||||
|
||||
SERVICE_NAME="video-player"
|
||||
RANDOM_SERVICE_NAME="video-player-random"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "Starting video player service..."
|
||||
sudo systemctl start $SERVICE_NAME
|
||||
sudo systemctl status $SERVICE_NAME
|
||||
;;
|
||||
stop)
|
||||
echo "Stopping video player service..."
|
||||
sudo systemctl stop $SERVICE_NAME
|
||||
sudo systemctl stop $RANDOM_SERVICE_NAME
|
||||
;;
|
||||
restart)
|
||||
echo "Restarting video player service..."
|
||||
sudo systemctl restart $SERVICE_NAME
|
||||
sudo systemctl status $SERVICE_NAME
|
||||
;;
|
||||
status)
|
||||
echo "Video player service status:"
|
||||
sudo systemctl status $SERVICE_NAME
|
||||
;;
|
||||
enable)
|
||||
echo "Enabling video player service for boot..."
|
||||
sudo systemctl enable $SERVICE_NAME
|
||||
sudo systemctl disable $RANDOM_SERVICE_NAME
|
||||
echo "Video player service enabled for boot"
|
||||
;;
|
||||
enable-random)
|
||||
echo "Enabling random video player service for boot..."
|
||||
sudo systemctl enable $RANDOM_SERVICE_NAME
|
||||
sudo systemctl disable $SERVICE_NAME
|
||||
echo "Random video player service enabled for boot"
|
||||
;;
|
||||
disable)
|
||||
echo "Disabling video player services..."
|
||||
sudo systemctl disable $SERVICE_NAME
|
||||
sudo systemctl disable $RANDOM_SERVICE_NAME
|
||||
echo "Video player services disabled"
|
||||
;;
|
||||
logs)
|
||||
echo "Video player service logs:"
|
||||
sudo journalctl -u $SERVICE_NAME -f
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status|enable|enable-random|disable|logs}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start - Start the video player service"
|
||||
echo " stop - Stop the video player service"
|
||||
echo " restart - Restart the video player service"
|
||||
echo " status - Show service status"
|
||||
echo " enable - Enable video player for boot (default channel)"
|
||||
echo " enable-random - Enable random video player for boot"
|
||||
echo " disable - Disable video player services from boot"
|
||||
echo " logs - Show service logs (follow mode)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,29 +1,32 @@
|
||||
{
|
||||
"1": {
|
||||
"number": 1,
|
||||
"name": "Sample Video 1",
|
||||
"path": "/home/pi/Videos/sample1.mp4",
|
||||
"description": "First sample video",
|
||||
"category": "general",
|
||||
"enabled": true,
|
||||
"priority": 0
|
||||
},
|
||||
"2": {
|
||||
"number": 2,
|
||||
"name": "Sample Video 2",
|
||||
"path": "/home/pi/Videos/sample2.mp4",
|
||||
"description": "Second sample video",
|
||||
"category": "general",
|
||||
"enabled": true,
|
||||
"priority": 0
|
||||
},
|
||||
"3": {
|
||||
"number": 3,
|
||||
"name": "Sample Video 3",
|
||||
"path": "/home/pi/Videos/sample3.mp4",
|
||||
"description": "Third sample video",
|
||||
"category": "general",
|
||||
"enabled": true,
|
||||
"priority": 0
|
||||
"_note": "This file is no longer used. Channels are now automatically assigned based on video files found in the video directory. Video files are assigned channel numbers 1, 2, 3... in alphabetical order.",
|
||||
"_example": {
|
||||
"1": {
|
||||
"number": 1,
|
||||
"name": "Sample Video 1",
|
||||
"path": "/home/pi/Videos/sample1.mp4",
|
||||
"description": "First sample video",
|
||||
"category": "general",
|
||||
"enabled": true,
|
||||
"priority": 0
|
||||
},
|
||||
"2": {
|
||||
"number": 2,
|
||||
"name": "Sample Video 2",
|
||||
"path": "/home/pi/Videos/sample2.mp4",
|
||||
"description": "Second sample video",
|
||||
"category": "general",
|
||||
"enabled": true,
|
||||
"priority": 0
|
||||
},
|
||||
"3": {
|
||||
"number": 3,
|
||||
"name": "Sample Video 3",
|
||||
"path": "/home/pi/Videos/sample3.mp4",
|
||||
"description": "Third sample video",
|
||||
"category": "general",
|
||||
"enabled": true,
|
||||
"priority": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"channel_timeout": 3.0,
|
||||
"multi_digit_timeout": 1.0,
|
||||
"channel_display_timeout": 2.0,
|
||||
"channel_refresh_interval": 30.0,
|
||||
"channel_assignment_method": "alphabetical",
|
||||
"ir_pin": 18,
|
||||
"ir_protocols": ["NEC", "RC5"],
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
[Unit]
|
||||
Description=Raspberry Pi Video Player with Random Video Startup
|
||||
Description=Raspberry Pi Video Player with Random Video Startup (No IR Remote Control)
|
||||
Documentation=https://github.com/your-repo/ulivision-tv
|
||||
After=network.target sound.target graphical-session.target
|
||||
Wants=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/ulivision-tv
|
||||
ExecStart=/usr/bin/python3 /home/pi/ulivision-tv/video_player.py --random
|
||||
User=tulivision
|
||||
Group=tulivision
|
||||
WorkingDirectory=/home/tulivision/rpi-tulivision
|
||||
ExecStart=/home/tulivision/rpi-tulivision/venv/bin/python3 /home/tulivision/rpi-tulivision/video_player.py --no-ir --random
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
ExecStop=/bin/kill -TERM $MAINPID
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutStopSec=30
|
||||
KillMode=mixed
|
||||
KillSignal=SIGTERM
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=video-player-random
|
||||
|
||||
# Environment variables
|
||||
Environment=DISPLAY=:0
|
||||
Environment=XAUTHORITY=/home/pi/.Xauthority
|
||||
Environment=PYTHONPATH=/home/pi/ulivision-tv
|
||||
Environment=HOME=/home/pi
|
||||
Environment=XAUTHORITY=/home/tulivision/.Xauthority
|
||||
Environment=PYTHONPATH=/home/tulivision/rpi-tulivision
|
||||
Environment=HOME=/home/tulivision
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=false
|
||||
@@ -33,8 +37,5 @@ ProtectHome=false
|
||||
LimitNOFILE=65536
|
||||
MemoryMax=512M
|
||||
|
||||
# GPIO access
|
||||
SupplementaryGroups=gpio
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
[Unit]
|
||||
Description=Raspberry Pi Video Player with IR Remote Control
|
||||
Description=Raspberry Pi Video Player (No IR Remote Control)
|
||||
Documentation=https://github.com/your-repo/ulivision-tv
|
||||
After=network.target sound.target graphical-session.target
|
||||
Wants=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/ulivision-tv
|
||||
ExecStart=/usr/bin/python3 /home/pi/ulivision-tv/video_player.py
|
||||
User=tulivision
|
||||
Group=tulivision
|
||||
WorkingDirectory=/home/tulivision/rpi-tulivision
|
||||
ExecStart=/home/tulivision/rpi-tulivision/venv/bin/python3 /home/tulivision/rpi-tulivision/video_player.py --no-ir
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
ExecStop=/bin/kill -TERM $MAINPID
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutStopSec=30
|
||||
KillMode=mixed
|
||||
KillSignal=SIGTERM
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=video-player
|
||||
|
||||
# Environment variables
|
||||
Environment=DISPLAY=:0
|
||||
Environment=XAUTHORITY=/home/pi/.Xauthority
|
||||
Environment=PYTHONPATH=/home/pi/ulivision-tv
|
||||
Environment=HOME=/home/pi
|
||||
Environment=XAUTHORITY=/home/tulivision/.Xauthority
|
||||
Environment=PYTHONPATH=/home/tulivision/rpi-tulivision
|
||||
Environment=HOME=/home/tulivision
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=false
|
||||
@@ -33,8 +37,5 @@ ProtectHome=false
|
||||
LimitNOFILE=65536
|
||||
MemoryMax=512M
|
||||
|
||||
# GPIO access
|
||||
SupplementaryGroups=gpio
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
379
video_player.py
379
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
|
||||
@@ -27,7 +28,7 @@ load_dotenv()
|
||||
class VideoPlayer:
|
||||
"""Main video player class with VLC integration and IR remote control"""
|
||||
|
||||
def __init__(self, config_path: str = "config.json"):
|
||||
def __init__(self, config_path: str = "config.json", enable_ir: bool = True):
|
||||
"""Initialize the video player with configuration"""
|
||||
self.config_path = config_path
|
||||
self.config = self.load_config()
|
||||
@@ -40,6 +41,7 @@ class VideoPlayer:
|
||||
self.current_channel = None
|
||||
self.ir_queue = queue.Queue()
|
||||
self.running = False
|
||||
self.enable_ir = enable_ir
|
||||
|
||||
# IR Remote settings
|
||||
self.ir_pin = self.config.get('ir_pin', 18)
|
||||
@@ -50,11 +52,20 @@ class VideoPlayer:
|
||||
self.default_channel = self.config.get('default_channel', 1)
|
||||
self.channel_timeout = self.config.get('channel_timeout', 3.0)
|
||||
self.multi_digit_timeout = self.config.get('multi_digit_timeout', 1.0)
|
||||
self.channel_refresh_interval = self.config.get('channel_refresh_interval', 30.0) # seconds
|
||||
|
||||
# Multi-digit channel input
|
||||
self.channel_input = ""
|
||||
self.last_digit_time = 0
|
||||
|
||||
# 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:
|
||||
@@ -78,34 +89,11 @@ class VideoPlayer:
|
||||
"default_channel": 1,
|
||||
"channel_timeout": 3.0,
|
||||
"multi_digit_timeout": 1.0,
|
||||
"channel_refresh_interval": 30.0,
|
||||
"vlc_options": [
|
||||
"--fullscreen",
|
||||
"--no-video-title-show",
|
||||
"--no-audio-display",
|
||||
"--always-on-top",
|
||||
"--video-on-top",
|
||||
"--no-embedded-video",
|
||||
"--no-video-deco",
|
||||
"--no-qt-fs-controller",
|
||||
"--no-qt-system-tray",
|
||||
"--no-qt-notification",
|
||||
"--no-qt-privacy-ask",
|
||||
"--no-qt-updates-notif",
|
||||
"--no-qt-error-dialogs",
|
||||
"--no-qt-fs-controller",
|
||||
"--no-qt-video-autosize",
|
||||
"--no-qt-name-in-title",
|
||||
"--no-qt-minimal-view",
|
||||
"--no-qt-bgcone",
|
||||
"--no-qt-pause-minimized",
|
||||
"--no-qt-continue",
|
||||
"--no-qt-recentplay",
|
||||
"--no-qt-start-minimized",
|
||||
"--no-qt-system-tray",
|
||||
"--no-qt-notification",
|
||||
"--no-qt-privacy-ask",
|
||||
"--no-qt-updates-notif",
|
||||
"--no-qt-error-dialogs"
|
||||
"--quiet"
|
||||
],
|
||||
"ir_codes": {
|
||||
"0": "channel_0",
|
||||
@@ -154,9 +142,17 @@ class VideoPlayer:
|
||||
"""Initialize VLC media player"""
|
||||
try:
|
||||
vlc_options = self.config.get('vlc_options', [])
|
||||
self.vlc_instance = vlc.Instance(vlc_options)
|
||||
|
||||
# Use the configured VLC options
|
||||
all_vlc_options = vlc_options
|
||||
|
||||
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)
|
||||
@@ -201,7 +197,7 @@ class VideoPlayer:
|
||||
return video_files
|
||||
|
||||
def create_channels(self):
|
||||
"""Create channel mapping from video files"""
|
||||
"""Create channel mapping from video files automatically"""
|
||||
video_files = self.scan_video_folder()
|
||||
self.channels = {}
|
||||
|
||||
@@ -213,52 +209,40 @@ class VideoPlayer:
|
||||
'file': video_file
|
||||
}
|
||||
|
||||
self.logger.info(f"Created {len(self.channels)} channels")
|
||||
|
||||
# Save channel mapping to file
|
||||
self.save_channel_mapping()
|
||||
self.logger.info(f"Created {len(self.channels)} channels automatically from directory")
|
||||
|
||||
def save_channel_mapping(self):
|
||||
"""Save channel mapping to JSON file"""
|
||||
def load_channels_from_directory(self):
|
||||
"""Load channels automatically from video directory"""
|
||||
try:
|
||||
channel_data = {}
|
||||
for channel_num, channel_info in self.channels.items():
|
||||
channel_data[str(channel_num)] = {
|
||||
'name': channel_info['name'],
|
||||
'path': channel_info['path']
|
||||
}
|
||||
|
||||
with open('channels.json', 'w') as f:
|
||||
json.dump(channel_data, f, indent=2)
|
||||
|
||||
self.logger.info("Channel mapping saved to channels.json")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error saving channel mapping: {e}")
|
||||
|
||||
def load_channel_mapping(self):
|
||||
"""Load channel mapping from JSON file"""
|
||||
try:
|
||||
if os.path.exists('channels.json'):
|
||||
with open('channels.json', 'r') as f:
|
||||
channel_data = json.load(f)
|
||||
|
||||
self.channels = {}
|
||||
for channel_num, channel_info in channel_data.items():
|
||||
video_path = Path(channel_info['path'])
|
||||
if video_path.exists():
|
||||
self.channels[int(channel_num)] = {
|
||||
'number': int(channel_num),
|
||||
'name': channel_info['name'],
|
||||
'path': channel_info['path'],
|
||||
'file': video_path
|
||||
}
|
||||
|
||||
self.logger.info(f"Loaded {len(self.channels)} channels from mapping file")
|
||||
self.create_channels()
|
||||
if self.channels:
|
||||
self.logger.info(f"Loaded {len(self.channels)} channels from directory")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning("No video files found in directory")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading channel mapping: {e}")
|
||||
self.logger.error(f"Error loading channels from directory: {e}")
|
||||
return False
|
||||
|
||||
def refresh_channels(self):
|
||||
"""Refresh channel list from directory (useful when videos are added/removed)"""
|
||||
old_channel_count = len(self.channels)
|
||||
self.load_channels_from_directory()
|
||||
new_channel_count = len(self.channels)
|
||||
|
||||
return False
|
||||
if new_channel_count != old_channel_count:
|
||||
self.logger.info(f"Channel count changed: {old_channel_count} -> {new_channel_count}")
|
||||
|
||||
# If current channel no longer exists, switch to first available
|
||||
if self.current_channel and self.current_channel not in self.channels:
|
||||
if self.channels:
|
||||
first_channel = min(self.channels.keys())
|
||||
self.logger.info(f"Current channel {self.current_channel} no longer exists, switching to channel {first_channel}")
|
||||
self.play_channel(first_channel)
|
||||
else:
|
||||
self.logger.warning("No channels available after refresh")
|
||||
self.current_channel = None
|
||||
|
||||
def play_channel(self, channel_number: int) -> bool:
|
||||
"""Play video for specified channel number"""
|
||||
@@ -327,6 +311,10 @@ class VideoPlayer:
|
||||
|
||||
def setup_gpio(self):
|
||||
"""Setup GPIO for IR receiver"""
|
||||
if not self.enable_ir:
|
||||
self.logger.info("IR remote control disabled, skipping GPIO setup")
|
||||
return True
|
||||
|
||||
try:
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(self.ir_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
@@ -494,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
|
||||
|
||||
@@ -515,17 +613,25 @@ class VideoPlayer:
|
||||
self.logger.error("Failed to setup GPIO")
|
||||
return False
|
||||
|
||||
# Load or create channels
|
||||
if not self.load_channel_mapping():
|
||||
self.create_channels()
|
||||
# Load channels automatically from directory
|
||||
if not self.load_channels_from_directory():
|
||||
self.logger.error("Failed to load channels from directory")
|
||||
return False
|
||||
|
||||
if not self.channels:
|
||||
self.logger.error("No video files found")
|
||||
return False
|
||||
|
||||
# Start IR processing thread
|
||||
ir_thread = threading.Thread(target=self.process_ir_commands, daemon=True)
|
||||
ir_thread.start()
|
||||
# Start IR processing thread only if IR is enabled
|
||||
if self.enable_ir:
|
||||
ir_thread = threading.Thread(target=self.process_ir_commands, daemon=True)
|
||||
ir_thread.start()
|
||||
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
|
||||
@@ -566,6 +672,12 @@ class VideoPlayer:
|
||||
self.logger.info("Video stopped, ensuring video is playing")
|
||||
self.ensure_video_playing()
|
||||
|
||||
# Periodically refresh channels to detect new/removed videos
|
||||
current_time = time.time()
|
||||
if current_time - self.last_channel_refresh > self.channel_refresh_interval:
|
||||
self.refresh_channels()
|
||||
self.last_channel_refresh = current_time
|
||||
|
||||
# Keep VLC on top
|
||||
self.keep_vlc_on_top()
|
||||
|
||||
@@ -578,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")
|
||||
|
||||
@@ -610,7 +726,23 @@ class VideoPlayer:
|
||||
if self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
|
||||
GPIO.cleanup()
|
||||
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():
|
||||
@@ -620,12 +752,18 @@ def parse_arguments():
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python3 video_player.py # Start with default channel
|
||||
python3 video_player.py # Start with default channel (auto-assigned)
|
||||
python3 video_player.py --random # Start with random video
|
||||
python3 video_player.py --channel 5 # Start with channel 5
|
||||
python3 video_player.py --channel 5 # Start with channel 5 (auto-assigned)
|
||||
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 --no-ir # Start without IR remote control
|
||||
python3 video_player.py --list-channels # List available channels (auto-assigned)
|
||||
python3 video_player.py --list-videos # List available videos with indices
|
||||
python3 video_player.py --refresh-channels # Refresh channel list from directory
|
||||
|
||||
Note: Channels are automatically assigned numbers (1, 2, 3...) based on alphabetical order
|
||||
of video files found in the video directory. No channel mapping file is needed.
|
||||
Use --no-ir to start without IR remote control if GPIO access is not available.
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -660,6 +798,11 @@ Examples:
|
||||
action='store_true',
|
||||
help='List available videos with indices and exit'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--refresh-channels',
|
||||
action='store_true',
|
||||
help='Refresh channel list from directory and exit'
|
||||
)
|
||||
|
||||
# Configuration arguments
|
||||
parser.add_argument(
|
||||
@@ -668,6 +811,11 @@ Examples:
|
||||
default='config.json',
|
||||
help='Path to configuration file (default: config.json)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-ir',
|
||||
action='store_true',
|
||||
help='Start without IR remote control (skip GPIO setup)'
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -704,37 +852,48 @@ def main():
|
||||
"""Main entry point"""
|
||||
args = parse_arguments()
|
||||
|
||||
# Check if user is in gpio group (needed for GPIO access)
|
||||
import grp
|
||||
try:
|
||||
gpio_group = grp.getgrnam('gpio')
|
||||
current_groups = os.getgroups()
|
||||
if gpio_group.gr_gid not in current_groups:
|
||||
print("This script requires GPIO access. Please add your user to the gpio group:")
|
||||
print("sudo usermod -a -G gpio $USER")
|
||||
print("Then log out and log back in, or run: newgrp gpio")
|
||||
# Check if user is in gpio group (needed for GPIO access) only if IR is enabled
|
||||
if not args.no_ir:
|
||||
import grp
|
||||
try:
|
||||
gpio_group = grp.getgrnam('gpio')
|
||||
current_groups = os.getgroups()
|
||||
if gpio_group.gr_gid not in current_groups:
|
||||
print("This script requires GPIO access. Please add your user to the gpio group:")
|
||||
print("sudo usermod -a -G gpio $USER")
|
||||
print("Then log out and log back in, or run: newgrp gpio")
|
||||
print("Alternatively, use --no-ir to start without IR remote control")
|
||||
sys.exit(1)
|
||||
except KeyError:
|
||||
print("GPIO group not found. Please ensure your system has GPIO support.")
|
||||
print("Alternatively, use --no-ir to start without IR remote control")
|
||||
sys.exit(1)
|
||||
except KeyError:
|
||||
print("GPIO group not found. Please ensure your system has GPIO support.")
|
||||
sys.exit(1)
|
||||
|
||||
# Check if another instance is running
|
||||
current_pid = os.getpid()
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||
try:
|
||||
if 'video_player.py' in ' '.join(proc.info['cmdline'] or []) and proc.info['pid'] != os.getpid():
|
||||
print("Another instance of video_player.py is already running")
|
||||
sys.exit(1)
|
||||
if proc.info['cmdline']:
|
||||
cmdline = proc.info['cmdline']
|
||||
# Check if this is actually running video_player.py (not just containing it in args)
|
||||
if (len(cmdline) > 1 and
|
||||
cmdline[1].endswith('video_player.py') and
|
||||
proc.info['pid'] != current_pid):
|
||||
print("Another instance of video_player.py is already running")
|
||||
sys.exit(1)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
# Create video player
|
||||
player = VideoPlayer(args.config)
|
||||
enable_ir = not args.no_ir
|
||||
player = VideoPlayer(args.config, enable_ir=enable_ir)
|
||||
|
||||
# Handle list commands
|
||||
if args.list_channels:
|
||||
# Load channels first
|
||||
if not player.load_channel_mapping():
|
||||
player.create_channels()
|
||||
# Load channels from directory
|
||||
if not player.load_channels_from_directory():
|
||||
print("No video files found in directory")
|
||||
return
|
||||
list_channels(player)
|
||||
return
|
||||
|
||||
@@ -742,6 +901,14 @@ def main():
|
||||
list_videos(player)
|
||||
return
|
||||
|
||||
if args.refresh_channels:
|
||||
if player.load_channels_from_directory():
|
||||
print(f"Refreshed channels: {len(player.channels)} channels found")
|
||||
list_channels(player)
|
||||
else:
|
||||
print("No video files found in directory")
|
||||
return
|
||||
|
||||
# Determine startup mode and parameters
|
||||
startup_mode = "default"
|
||||
channel_number = None
|
||||
|
||||
Reference in New Issue
Block a user