Compare commits

..

17 Commits

Author SHA1 Message Date
21722c5c85 Add local Pillow build support and update 32-bit download script for ARMhf compatibility 2025-09-11 13:36:57 +03:00
ad4d71e0b6 Remove requirements.txt - replaced by install scripts with proper offline library management 2025-09-11 10:46:16 +03:00
d7f7df49e7 Fix permissions on download_32bit_libs.sh 2025-09-11 10:15:38 +03:00
9c42b38c4b Fix 32-bit bcrypt ELFCLASS64 error and enhance installation
- Enhanced install_32bit.sh with clean virtual environment creation
- Added --force-reinstall and --clear flags for proper isolation
- Improved download_32bit_libs.sh with explicit armv7l targeting
- Added troubleshoot_32bit.sh for diagnosing 32-bit issues
- Better architecture verification and error messages
- Comprehensive package testing after installation

Fixes ELFCLASS64 error on 32-bit systems by ensuring proper
architecture isolation and clean package installation.
2025-09-11 10:15:25 +03:00
0aa1bb7069 Clean up project: Remove test files and add dedicated 32-bit installer
- Remove temporary test files (test_*.py)
- Remove unused installation scripts
- Add install_32bit.sh for dedicated 32-bit Raspberry Pi OS installation
- Clean up repository structure for production deployment
2025-09-11 09:00:15 +03:00
35db99eb3d installer offline 2025-09-10 16:19:43 +03:00
b6e6190d6c create installer 2025-09-10 15:43:14 +03:00
26a9db889f deleted unnecesary files 2025-09-10 14:30:51 +03:00
26fc946a65 updated to show when is not online 2025-09-09 17:11:25 +03:00
a91b07ede4 updated 2025-09-09 16:16:23 +03:00
185f3099ad updated version 2025-09-08 15:46:59 +03:00
5063b47a56 updated feedback 2025-09-08 15:19:47 +03:00
e2eecb9cf9 updated to send feedback 2025-09-08 13:19:50 +03:00
bd4f101fcc updating player startup 2025-09-05 12:33:47 +03:00
cb861d0ffa Complete auto-startup installation system for Raspberry Pi Zero
- Enhanced install_minimal_xorg.sh with full automatic startup configuration
- Added systemd service for robust signage player auto-start
- Configured autologin and X server auto-launch on boot
- Added multiple startup methods (systemd, bashrc, rc.local) for reliability
- Disabled screen blanking and power management for kiosk operation
- Updated requirements.txt with proper version specifications
- Fixed run_tkinter_debug.sh for correct directory structure
- Added comprehensive logging and error handling
- Optimized for headless Raspberry Pi Zero deployment

Features:
 Plug-and-play operation - boots directly to signage player
 Automatic restart on crashes
 Multiple fallback startup methods
 Complete dependency installation
 Service management commands
 Hardware optimizations for digital signage
2025-09-04 16:44:00 +03:00
02d13b2eaa Enhanced player stability and added exit confirmation
- Fixed VLC display errors by implementing PIL fallback for images
- Added comprehensive timer management with individual error handling
- Implemented watchdog timers to prevent freezing during media transitions
- Enhanced exit functionality with quickconnect code confirmation dialog
- Improved settings window behavior with proper modal focus
- Added transition protection to prevent rapid media cycling
- Enhanced error handling throughout the application
- Fixed controls window cleanup and destruction process
2025-09-04 16:32:48 +03:00
d2a996feb9 Fix player freezing and rapid cycling issues
- Switch from VLC to PIL for image display to avoid VLC display errors
- Add transition protection to prevent rapid media cycling
- Remove conflicting next_media_loop calls
- Improve error handling and logging for better debugging
- Fix VLC configuration for better Raspberry Pi compatibility
2025-09-04 15:52:13 +03:00
68 changed files with 23350 additions and 3575 deletions

View File

@@ -0,0 +1,59 @@
# Multi-Architecture Offline Installation Guide
## Overview
The enhanced `install_offline.sh` script automatically detects your system architecture and uses the appropriate libraries.
## Architecture Support
### 64-bit ARM (aarch64) - Raspberry Pi 4/5 with 64-bit OS
- **Folder used**: `req_libraries/`
- **Files**: 18 wheel files
- **Key packages**:
- bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl
- pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.whl
- psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.whl
### 32-bit ARM (armv7l) - Raspberry Pi with 32-bit OS
- **Folder used**: `req_libraries_32bit/`
- **Files**: 18 wheel files
- **Key packages**:
- bcrypt-4.3.0-cp311-cp311-linux_armv7l.whl
- pillow-11.3.0-cp311-cp311-linux_armv7l.whl
- psutil-6.0.0-cp311-abi3-linux_armv7l.whl
## Usage
### Simple Installation (Auto-Detect)
```bash
./install_offline.sh
```
The script will:
1. Detect your architecture automatically
2. Choose the correct library folder
3. Install the appropriate packages
4. Create a virtual environment
5. Verify installation
### Manual Architecture Check
```bash
uname -m
```
- `aarch64` = 64-bit (uses req_libraries/)
- `armv7l` = 32-bit (uses req_libraries_32bit/)
## Folder Structure
```
tkinter_player/
├── req_libraries/ # 64-bit ARM wheels
├── req_libraries_32bit/ # 32-bit ARM wheels
├── install_offline.sh # Auto-detecting installer
└── requirements.txt # Package list
```
## Benefits
- ✅ Single installer for both architectures
- ✅ Automatic architecture detection
- ✅ No manual intervention needed
- ✅ Proper error messages for missing libraries
- ✅ Complete offline installation support

View File

@@ -1,84 +0,0 @@
Here is the complete content for the `How to use.txt` file, starting from **point 3** and including all the missing information:
```plaintext
### How to Install, Start, and Use the Signage Player Application
This guide provides step-by-step instructions for installing, starting, and using the signage player application.
---
## 3. Using the Application
### MediaPlayer Screen
- **Play/Pause Button**:
- Toggles between playing and pausing the media.
- Automatically resumes playback after 30 seconds if not toggled again.
- **Next/Previous Buttons**:
- Navigate to the next or previous media in the playlist.
- **Exit App Button**:
- Opens a password-protected popup. Enter the `quickconnect_key` from the configuration file to exit the app.
### Settings Screen
- **Configuration Fields**:
- Update fields like `screen_orientation`, `screen_name`, `quickconnect_key`, `server_ip`, and `port`.
- **Save Button**:
- Saves the updated configuration and returns to the MediaPlayer screen.
- **Exit App Button**:
- Opens a password-protected popup. Enter the `quickconnect_key` to exit the app.
---
## 4. Configuration
### Configuration File
The configuration file is located at:
```bash
app_config.txt
```
### Example Configuration
```json
{
"screen_orientation": "Landscape",
"screen_name": "MyScreen",
"quickconnect_key": "12345",
"server_ip": "192.168.1.1",
"port": "8080"
}
```
### Updating Configuration
1. Navigate to the **Settings Screen** in the app.
2. Update the fields and click **Save**.
3. The configuration will be saved to `app_config.txt`.
---
## 5. Troubleshooting
### Permission Denied for `run_app.sh`
- Ensure the script is executable:
```bash
chmod +x /home/pi/Desktop/signage-player/run_app.sh
```
### Application Does Not Start on Boot
- Verify the `~/.bashrc` or `crontab` entry is correct.
- Check the script's permissions and paths.
### Media Files Not Playing
- Ensure media files are downloaded to the correct directory:
```bash
/home/pi/Desktop/signage-player/src/static/resurse
```
- Check the playlist configuration and server connection.
### Password for Exit App
- The password is the `quickconnect_key` from the configuration file (`app_config.txt`).
---
Let me know if you need further clarification or additional details!
```

44
OFFLINE_INSTALL_README.md Normal file
View File

@@ -0,0 +1,44 @@
# Offline Installation Guide
## Architecture Compatibility
### Current Libraries (req_libraries/)
- Compatible with: 64-bit Raspberry Pi OS (aarch64)
- NOT compatible with: 32-bit Raspberry Pi OS (armv7l)
### For 32-bit Raspberry Pi OS
The current wheels contain aarch64 packages that won't work on 32-bit systems.
## Installation Options
### 64-bit Raspberry Pi OS:
```bash
./install_offline.sh
```
### 32-bit Raspberry Pi OS:
1. Run on internet-connected system: `./download_32bit_libs.sh`
2. Copy req_libraries_32bit/ to offline system
3. Rename: `mv req_libraries_32bit req_libraries`
4. Run: `./install_offline.sh`
## System Requirements
### 32-bit systems need build tools:
```bash
sudo apt update
sudo apt install build-essential python3-dev python3-pip
```
### All systems need VLC:
```bash
sudo apt install vlc
```
## Quick Check
```bash
uname -m
```
- aarch64 = 64-bit (current wheels work)
- armv7l = 32-bit (need different wheels)

150
README.md
View File

@@ -1,150 +0,0 @@
# 🎥 Kivy Media Player
A media player application built using **Kivy** that allows users to play video files, display images, and manage settings for quick connect codes and server configurations. The application checks for updates to the playlist every five minutes while running in full-screen mode.
---
## 📂 Project Structure
```
signage-player
├── src
│ ├── media_player.py # Handles media playback
│ ├── python_functions.py # Utility functions for downloading files and updating playlists
│ ├── kv
│ │ └── media_player.kv # Layout for the media player screen
│ ├── Resurse
│ │ ├── app_config.txt # Configuration file
│ │ ├── log.txt # Log file for media events
│ │ ├── play.png # Play button icon
│ │ ├── pause.png # Pause button icon
│ │ └── other icons... # Additional icons for the UI
│ └── static
│ └── resurse # Directory for media files (images/videos)
├── requirements.txt # Project dependencies
├── run_app.sh # Script to run the application
├── install.sh # Installation script
├── How to use.txt # Detailed usage instructions
└── README.md # Documentation for the project
```
---
## 🚀 Setup Instructions
### Prerequisites
Before installing the application, ensure the following are installed on your Raspberry Pi:
- **Python 3.7 or higher**
- **pip3** (Python package manager)
- **ffmpeg** (for video conversion)
- **Internet connection** for downloading dependencies
### Installation Steps
1. **Clone the Repository**
```bash
git clone https://gitea.moto-adv.com/ske087/signage-player.git
cd signage-player
```
2. **Run the Installation Script**
```bash
./install.sh
```
The installation script will:
- Update the system.
- Install required Python and system dependencies.
- Clone the repository to `/home/pi/Desktop/ds-player`.
- Add the run_app.sh script to `~/.bashrc` for autostart.
- Make the run_app.sh script executable.
3. **Reboot the Device**
After installation, reboot the Raspberry Pi to start the application automatically:
```bash
sudo reboot
```
---
## 🎮 How to Use
### MediaPlayer Screen
- **▶️ Play/Pause Button**:
- Toggles between playing and pausing the media.
- Automatically resumes playback after 30 seconds if not toggled again.
- **⏩ Next/⏪ Previous Buttons**:
- Navigate to the next or previous media in the playlist.
- **❌ Exit App Button**:
- Opens a password-protected popup. Enter the `quickconnect_key` from the configuration file to exit the app.
### Settings Screen
- **Configuration Fields**:
- Update fields like `screen_orientation`, `screen_name`, `quickconnect_key`, `server_ip`, and `port`.
- **💾 Save Button**:
- Saves the updated configuration and returns to the MediaPlayer screen.
- **❌ Exit App Button**:
- Opens a password-protected popup. Enter the `quickconnect_key` to exit the app.
---
## ⚙️ Configuration
### Configuration File
The configuration file is located at:
```bash
/home/pi/Desktop/signage-player/src/Resurse/app_config.txt
```
### Example Configuration
```json
{
"screen_orientation": "Landscape",
"screen_name": "tv-panou1",
"quickconnect_key": "8887779",
"server_ip": "172.20.10.9",
"port": "80"
}
```
### Updating Configuration
1. Navigate to the **Settings Screen** in the app.
2. Update the fields and click **Save**.
3. The configuration will be saved to app_config.txt.
---
## 🛠️ Troubleshooting
### Permission Denied for run_app.sh
- Ensure the script is executable:
```bash
chmod +x /home/pi/Desktop/signage-player/run_app.sh
```
### Application Does Not Start on Boot
- Verify the `~/.bashrc` entry is correct.
- Check the script's permissions and paths.
### Media Files Not Playing
- Ensure media files are downloaded to the correct directory:
```bash
/home/pi/Desktop/signage-player/src/static/resurse
```
- Check the playlist configuration and server connection.
### Password for Exit App
- The password is the `quickconnect_key` from the configuration file (`app_config.txt`).
---
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a pull request or open an issue for any suggestions or improvements.
---
## 📜 License
This project is licensed under the **MIT License**. See the `LICENSE` file for more details.
````

29
build_pillow_local.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Build Pillow from source and place wheel in req_libraries_32bit
set -e
echo "======================================="
echo " BUILD PILLOW FROM SOURCE (32-bit) "
echo "======================================="
# Ensure virtual environment is activated
echo "Activating .venv..."
source .venv/bin/activate
# Find Pillow source tar.gz
PILLOW_SRC=$(ls req_libraries_32bit/Pillow-*.tar.gz 2>/dev/null | head -1)
if [ -z "$PILLOW_SRC" ]; then
echo "❌ Pillow source distribution not found in req_libraries_32bit."
exit 1
fi
echo "Building Pillow from source: $PILLOW_SRC"
pip wheel "$PILLOW_SRC" -w req_libraries_32bit
if ls req_libraries_32bit/pillow*.whl >/dev/null 2>&1; then
echo "✅ Pillow wheel built successfully!"
echo "You can now install it offline with ./install_32bit.sh."
else
echo "❌ Pillow wheel build failed. Check build dependencies."
fi

89
download_32bit_libs.sh Executable file
View File

@@ -0,0 +1,89 @@
#!/bin/bash
# Download 32-bit ARM compatible libraries with forced architecture
set -e
echo "======================================="
echo " 32-BIT ARM LIBRARY DOWNLOADER"
echo "======================================="
echo ""
# Remove existing 32-bit folder to ensure clean download
if [ -d "req_libraries_32bit" ]; then
echo "🗑️ Removing existing req_libraries_32bit for clean download..."
rm -rf req_libraries_32bit
fi
# Create fresh directory
mkdir -p req_libraries_32bit
echo "📦 Downloading 32-bit ARM compatible packages..."
echo " Target architecture: linux_armv7l"
echo ""
# Download with explicit 32-bit ARM platform targeting
echo "🔄 Downloading with forced 32-bit architecture (armv7l and armhf)..."
# Download for armv7l (wheels only)
pip download -r requirements.txt -d req_libraries_32bit/ \
--platform linux_armv7l \
--only-binary=:all: \
--python-version 311 \
--abi cp311 || true
# Download for armhf (wheels only)
pip download -r requirements.txt -d req_libraries_32bit/ \
--platform linux_armhf \
--only-binary=:all: \
--python-version 311 \
--abi cp311 || true
# Download Pillow as source if wheel is not available
echo "🔄 Downloading Pillow source distribution for local build..."
pip download Pillow -d req_libraries_32bit/ --no-binary=:all: || true
echo ""
echo "📊 Download results:"
WHEEL_COUNT=$(ls req_libraries_32bit/*.whl 2>/dev/null | wc -l || echo "0")
echo " Wheel files: $WHEEL_COUNT"
if [ "$WHEEL_COUNT" -gt 0 ]; then
echo ""
echo "🔍 Key 32-bit packages:"
if ls req_libraries_32bit/bcrypt*.whl >/dev/null 2>&1; then
BCRYPT_FILE=$(basename $(ls req_libraries_32bit/bcrypt*.whl | head -1))
echo " 🔐 BCRYPT: $BCRYPT_FILE"
if [[ "$BCRYPT_FILE" == *"armv7l"* ]]; then
echo " ✅ Correct 32-bit architecture"
else
echo " ⚠️ Architecture unclear"
fi
fi
if ls req_libraries_32bit/pillow*.whl >/dev/null 2>&1; then
PILLOW_FILE=$(basename $(ls req_libraries_32bit/pillow*.whl | head -1))
echo " 🖼️ PILLOW: $PILLOW_FILE"
if [[ "$PILLOW_FILE" == *"armv7l"* ]]; then
echo " ✅ Correct 32-bit architecture"
fi
elif ls req_libraries_32bit/Pillow-*.tar.gz >/dev/null 2>&1; then
PILLOW_SRC=$(basename $(ls req_libraries_32bit/Pillow-*.tar.gz | head -1))
echo " 🖼️ PILLOW source: $PILLOW_SRC"
echo " ⚠️ No wheel available for 32-bit ARM. Will need to build locally."
fi
echo ""
echo "✅ 32-bit libraries download completed!"
echo ""
echo "📋 Next steps:"
echo " 1. Copy this folder to your 32-bit Raspberry Pi"
echo " 2. Run: ./install_32bit.sh"
echo " 3. If Pillow wheel is missing, run: ./build_pillow_local.sh (see instructions below)"
else
echo "❌ No wheel files downloaded"
echo ""
echo "💡 Troubleshooting:"
echo " • Check internet connection"
echo " • Verify requirements.txt exists"
echo " • Some packages may not have 32-bit wheels available"
fi

107
install_32bit.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/bin/bash
# Dedicated Offline Installer for 32-bit Raspberry Pi OS (armv7l)
# Ensures clean installation with proper architecture isolation
set -e
echo "================================="
echo " TKINTER PLAYER 32-BIT INSTALL "
echo "================================="
# 1. Check architecture
ARCH=$(uname -m)
echo "Architecture: $ARCH"
if [ "$ARCH" != "armv7l" ]; then
echo "ERROR: This script is for 32-bit Raspberry Pi OS (armv7l) only!"
echo "Current architecture: $ARCH"
exit 1
fi
# 2. Check library folder
LIBS_FOLDER="req_libraries_32bit"
if [ ! -d "$LIBS_FOLDER" ]; then
echo "ERROR: $LIBS_FOLDER not found!"
echo "Please download 32-bit libraries first."
exit 1
fi
echo "Library folder: $LIBS_FOLDER"
WHEEL_COUNT=$(ls $LIBS_FOLDER/*.whl | wc -l)
echo "Wheel files: $WHEEL_COUNT"
# 3. Show key 32-bit packages
echo ""
echo "32-bit packages to install:"
if ls $LIBS_FOLDER/bcrypt*.whl >/dev/null 2>&1; then
BCRYPT_FILE=$(basename $(ls $LIBS_FOLDER/bcrypt*.whl | head -1))
echo " BCRYPT: $BCRYPT_FILE"
fi
echo ""
# 4. Clean existing virtual environment
if [ -d ".venv" ]; then
echo "Removing existing .venv to ensure clean installation..."
rm -rf .venv
fi
# 5. Create fresh virtual environment
echo "Creating fresh virtual environment..."
python3 -m venv .venv --clear
# 6. Activate environment
echo "Activating virtual environment..."
source .venv/bin/activate
# 7. Ensure we're using the virtual environment
echo "Python location: $(which python)"
echo "Pip location: $(which pip)"
# 8. Upgrade pip
echo "Upgrading pip..."
pip install --upgrade pip
# 9. Install packages with forced 32-bit isolation
echo ""
echo "Installing 32-bit packages (completely isolated)..."
pip install --no-index --no-deps --force-reinstall --find-links $LIBS_FOLDER/ $LIBS_FOLDER/*.whl
# 10. Verify 32-bit installation
echo ""
echo "Verifying 32-bit installation..."
python3 -c "
import sys
print(f'Python executable: {sys.executable}')
print(f'Architecture: $(uname -m)')
try:
import bcrypt
print('✅ bcrypt imported successfully')
# Test bcrypt functionality
password = b'test'
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
if bcrypt.checkpw(password, hashed):
print('✅ bcrypt functionality test passed')
else:
print('❌ bcrypt functionality test failed')
except Exception as e:
print(f'❌ bcrypt import/test failed: {e}')
try:
import requests
print('✅ requests imported successfully')
except Exception as e:
print(f'❌ requests failed: {e}')
try:
import vlc
print('✅ python-vlc imported successfully')
except Exception as e:
print(f'❌ python-vlc failed: {e}')
"
echo ""
echo "✅ 32-bit installation completed!"
echo ""
echo "Next steps:"
echo "1. source .venv/bin/activate"
echo "2. ./run_app.sh"

View File

@@ -1,34 +0,0 @@
#!/bin/bash
# Minimal installer for Raspberry Pi OS (no desktop environment)
# Installs Xorg, Openbox, disables power saving, and configures auto-launch of the signage player
set -e
USER_HOME="/home/pi"
PROJECT_DIR="$USER_HOME/Desktop/signage-player"
APP_LAUNCH_SCRIPT="$PROJECT_DIR/run_tkinter_app.sh"
# Update system
sudo apt update
sudo apt upgrade -y
# Install minimal X server and Openbox
sudo apt install -y xorg openbox
# Install VLC for video playback
sudo apt install -y vlc
# Install Python and dependencies
sudo apt install -y python3 python3-pip python3-venv python3-tk ffmpeg libopencv-dev python3-opencv \
libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev \
libjpeg-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev
# Create virtual environment and install Python requirements
cd "$PROJECT_DIR"
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r tkinter_requirements.txt
pip install python-vlc
chmod +x "$APP_LAUNCH_SCRIPT"
deactivate

68
install_offline.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Simple Offline Installation Script for Tkinter Player
set -e
echo "================================="
echo " TKINTER PLAYER OFFLINE INSTALL"
echo "================================="
# 1. Check architecture
# Detect architecture, treating armhf and armv7l as 32-bit
ARCH=$(uname -m)
if lscpu | grep -qi 'armhf'; then
ARCH_TYPE="armhf"
else
ARCH_TYPE="$ARCH"
fi
echo "Architecture: $ARCH_TYPE"
# 2. Select library folder
# Use 32-bit libraries for armv7l or armhf
if [ "$ARCH_TYPE" = "armv7l" ] || [ "$ARCH_TYPE" = "armhf" ]; then
LIBS_FOLDER="req_libraries_32bit"
echo "Using: 32-bit libraries (armv7l/armhf)"
elif [ "$ARCH_TYPE" = "aarch64" ]; then
LIBS_FOLDER="req_libraries"
echo "Using: 64-bit libraries (aarch64)"
else
LIBS_FOLDER="req_libraries"
echo "Using: default libraries"
fi
# 3. Check folder exists
if [ ! -d "$LIBS_FOLDER" ]; then
echo "ERROR: $LIBS_FOLDER not found!"
exit 1
fi
echo "Library folder: $LIBS_FOLDER"
WHEEL_COUNT=$(ls $LIBS_FOLDER/*.whl | wc -l)
echo "Wheel files: $WHEEL_COUNT"
# 4. Create .venv
echo ""
echo "Creating .venv..."
python3 -m venv .venv
# 5. Activate environment
echo "Activating environment..."
source .venv/bin/activate
# 6. Upgrade pip
#echo "Upgrading pip..."
#pip install --upgrade pip
# 7. Install packages offline
echo ""
echo "Installing from $LIBS_FOLDER..."
pip install --no-index --no-deps --find-links $LIBS_FOLDER/ $LIBS_FOLDER/*.whl
echo ""
echo "✅ Installation completed!"
echo ""
echo "Next steps:"
echo "1. source .venv/bin/activate"
echo "2. ./run_app.sh"

View File

@@ -1,68 +0,0 @@
#!/bin/bash
# Tkinter Media Player Installation Script
echo "Installing Tkinter Media Player..."
# Update system packages
echo "Updating system packages..."
sudo apt update
sudo apt upgrade -y
# Install system dependencies
echo "Installing system dependencies..."
sudo apt install -y python3 python3-pip python3-venv python3-tk
sudo apt install -y ffmpeg libopencv-dev python3-opencv
sudo apt install -y libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev
sudo apt install -y libjpeg-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev
# Create project directory if it doesn't exist
PROJECT_DIR="/home/pi/Desktop/tkinter_player"
if [ ! -d "$PROJECT_DIR" ]; then
echo "Project directory not found. Please ensure the tkinter_player directory exists."
exit 1
fi
cd "$PROJECT_DIR"
# Create virtual environment
echo "Creating Python virtual environment..."
python3 -m venv venv
# Activate virtual environment and install requirements
echo "Installing Python dependencies..."
source venv/bin/activate
pip install --upgrade pip
pip install -r tkinter_requirements.txt
deactivate
# Make launcher script executable
chmod +x run_tkinter_app.sh
# Create systemd service for auto-start
echo "Creating systemd service..."
sudo tee /etc/systemd/system/tkinter-signage-player.service > /dev/null <<EOF
[Unit]
Description=Tkinter Signage Player
After=graphical-session.target
[Service]
Type=simple
User=pi
Environment=DISPLAY=:0
ExecStart=/home/pi/Desktop/signage-player/run_tkinter_app.sh
Restart=always
RestartSec=10
[Install]
WantedBy=graphical-session.target
EOF
# Enable the service
sudo systemctl daemon-reload
sudo systemctl enable tkinter-signage-player.service
echo "Installation completed!"
echo "The tkinter media player will start automatically on boot."
echo "To start manually, run: ./run_tkinter_app.sh"
echo "To stop the service: sudo systemctl stop tkinter-signage-player.service"
echo "To view logs: sudo journalctl -u tkinter-signage-player.service -f"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,7 @@
# Core requirements for tkinter_player signage app
python-vlc
Pillow
pyautogui
requests
bcrypt
python-vlc
pyautogui
setuptools
wheel

35
run_app.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# Launch script for the tkinter signage player
# Get script directory and navigate there
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "=== Tkinter Signage Player Launcher ==="
echo "Working directory: $SCRIPT_DIR"
# Activate virtual environment
if [ -d ".venv" ]; then
echo "Activating virtual environment..."
source .venv/bin/activate
echo "Virtual environment activated"
else
echo "Warning: No virtual environment found"
fi
# Set display environment
export DISPLAY=${DISPLAY:-:0.0}
# Create directories if needed
mkdir -p signage_player/main_data
# Change to signage_player directory
cd signage_player
echo "Starting application..."
python main.py
# Deactivate venv when done
if [ -d "../.venv" ]; then
deactivate
fi

View File

@@ -1,14 +1,40 @@
#!/bin/bash
# Debugging launch script for the tkinter player application
# Activate the virtual environment
source venv/bin/activate
# Set working directory to script location
cd "$(dirname "$0")"
# Change to the tkinter app src directory
cd signage_player/src
# Activate the virtual environment if it exists
if [ -d "venv" ]; then
source venv/bin/activate
echo "Virtual environment activated"
else
echo "Warning: No virtual environment found, using system Python"
fi
# Set environment variables for better display on Raspberry Pi
export DISPLAY=${DISPLAY:-:0.0}
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp/runtime-pi}
# Change to the signage_player directory
cd signage_player
# Run the main application with full error output
python main.py
echo "Starting tkinter player..."
echo "Working directory: $(pwd)"
echo "Python path: $(which python3)"
echo "Display: $DISPLAY"
# Run with error logging
python3 main.py 2>&1 | tee ../main_data/app_debug.log
# Capture exit code
EXIT_CODE=$?
# Deactivate virtual environment when done
deactivate
if [ -d "../venv" ]; then
deactivate
fi
echo "Application exited with code: $EXIT_CODE"
exit $EXIT_CODE

View File

@@ -12,14 +12,34 @@ class AppSettingsWindow(tk.Tk):
self.geometry('440x600') # Increased height for better button visibility
self.resizable(False, False)
self.config(bg='#23272e')
# Ensure window appears on top and gets focus
self.attributes('-topmost', True)
self.lift()
self.focus_force()
self.grab_set() # Make window modal
# Center the window on screen
self.center_window()
self.fields = {}
self.load_config()
self.style = ttk.Style(self)
self.set_styles()
self.create_widgets()
# Ensure focus after widgets are created
self.after(100, self.focus_force)
def center_window(self):
"""Center the settings window on the screen"""
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f'{width}x{height}+{x}+{y}')
def set_styles(self):
self.style.theme_use('clam')
self.style.configure('TLabel', background='#23272e', foreground='#e0e0e0', font=('Segoe UI', 13, 'bold'))

View File

@@ -3,8 +3,204 @@ import json
import requests
import bcrypt
import re
import datetime
from logging_config import Logger
# Global variable to track server connectivity status
SERVER_CONNECTION_STATUS = {
'is_online': True,
'last_successful_connection': None,
'last_playlist_update': None,
'error_message': None
}
def get_server_status():
"""Get current server connection status"""
return SERVER_CONNECTION_STATUS.copy()
def get_last_playlist_update_time():
"""Get the timestamp of the last playlist update from filesystem"""
try:
playlist_dir = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
if os.path.exists(playlist_dir):
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
if playlist_files:
# Get the most recent playlist file
latest_file = max([os.path.join(playlist_dir, f) for f in playlist_files], key=os.path.getmtime)
mod_time = os.path.getmtime(latest_file)
return datetime.datetime.fromtimestamp(mod_time)
return None
except Exception as e:
Logger.error(f"Error getting last playlist update time: {e}")
return None
def set_server_offline(error_message=None):
"""Mark server as offline with optional error message"""
global SERVER_CONNECTION_STATUS
SERVER_CONNECTION_STATUS['is_online'] = False
SERVER_CONNECTION_STATUS['error_message'] = error_message
Logger.warning(f"Server marked as offline: {error_message}")
def set_server_online():
"""Mark server as online and update connection time"""
global SERVER_CONNECTION_STATUS
SERVER_CONNECTION_STATUS['is_online'] = True
SERVER_CONNECTION_STATUS['last_successful_connection'] = datetime.datetime.now()
SERVER_CONNECTION_STATUS['error_message'] = None
Logger.info("Server connection restored")
def send_player_feedback(config, message, status="active", playlist_version=None, error_details=None):
"""
Send feedback to the server about player status.
Args:
config (dict): Configuration containing server details
message (str): Main feedback message
status (str): Player status - "active", "playing", "error", "restarting"
playlist_version (int, optional): Current playlist version being played
error_details (str, optional): Error details if status is "error"
Returns:
bool: True if feedback sent successfully, False otherwise
"""
try:
server = config.get("server_ip", "")
host = config.get("screen_name", "")
quick = config.get("quickconnect_key", "")
port = config.get("port", "")
# Construct server URL
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
if re.match(ip_pattern, server):
feedback_url = f'http://{server}:{port}/api/player-feedback'
else:
feedback_url = f'http://{server}/api/player-feedback'
# Prepare feedback data
feedback_data = {
'player_name': host,
'quickconnect_code': quick,
'message': message,
'status': status,
'timestamp': datetime.datetime.now().isoformat(),
'playlist_version': playlist_version,
'error_details': error_details
}
Logger.info(f"Sending feedback to {feedback_url}: {feedback_data}")
# Send POST request
response = requests.post(feedback_url, json=feedback_data, timeout=10)
if response.status_code == 200:
Logger.info(f"Feedback sent successfully: {message}")
# Mark server as online on successful feedback
set_server_online()
return True
else:
Logger.warning(f"Feedback failed with status {response.status_code}: {response.text}")
set_server_offline(f"Feedback failed with status {response.status_code}")
return False
except requests.exceptions.RequestException as e:
Logger.error(f"Failed to send feedback: {e}")
set_server_offline(f"Network error during feedback: {e}")
return False
except Exception as e:
Logger.error(f"Unexpected error sending feedback: {e}")
set_server_offline(f"Unexpected error during feedback: {e}")
return False
def send_playlist_check_feedback(config, playlist_version=None):
"""
Send feedback when server is interrogated for playlist updates.
Args:
config (dict): Configuration containing server details
playlist_version (int, optional): Current playlist version
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
message = f"player {player_name}, server interrogation, checking for updates : {version_info}"
return send_player_feedback(
config=config,
message=message,
status="active",
playlist_version=playlist_version
)
def send_playlist_restart_feedback(config, playlist_version=None):
"""
Send feedback when playlist loop ends and restarts.
Args:
config (dict): Configuration containing server details
playlist_version (int, optional): Current playlist version
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
message = f"player {player_name}, playlist working in loop, cycle completed : {version_info}"
return send_player_feedback(
config=config,
message=message,
status="restarting",
playlist_version=playlist_version
)
def send_player_error_feedback(config, error_message, playlist_version=None):
"""
Send feedback when an error occurs in the player.
Args:
config (dict): Configuration containing server details
error_message (str): Description of the error
playlist_version (int, optional): Current playlist version
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
message = f"player {player_name}, error occurred"
return send_player_feedback(
config=config,
message=message,
status="error",
playlist_version=playlist_version,
error_details=error_message
)
def send_playing_status_feedback(config, playlist_version=None, current_media=None):
"""
Send feedback about playlist starting (first media).
Args:
config (dict): Configuration containing server details
playlist_version (int, optional): Current playlist version
current_media (str, optional): First media file in playlist
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
message = f"player {player_name}, playlist started : {version_info}"
return send_player_feedback(
config=config,
message=message,
status="playing",
playlist_version=playlist_version
)
def is_playlist_up_to_date(local_playlist_path, config):
"""
Compare the version of the local playlist with the server playlist.
@@ -39,25 +235,44 @@ def fetch_server_playlist(config):
'quickconnect_code': quick
}
Logger.info(f"Fetching playlist from URL: {server_url} with params: {params}")
response = requests.get(server_url, params=params)
response = requests.get(server_url, params=params, timeout=15)
if response.status_code == 200:
response_data = response.json()
Logger.info(f"Server response: {response_data}")
playlist = response_data.get('playlist', [])
version = response_data.get('playlist_version', None)
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
if version is not None and hashed_quickconnect is not None:
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
Logger.info("Fetched updated playlist from server.")
# Mark server as online on successful connection
set_server_online()
return {'playlist': playlist, 'version': version}
else:
Logger.error("Quickconnect code validation failed.")
set_server_offline("Authentication failed - invalid quickconnect code")
else:
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
set_server_offline("Invalid server response - missing playlist data")
else:
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
set_server_offline(f"Server returned error code: {response.status_code}")
except requests.exceptions.ConnectTimeout as e:
Logger.error(f"Connection timeout while fetching playlist: {e}")
set_server_offline("Connection timeout - server unreachable")
except requests.exceptions.ConnectionError as e:
Logger.error(f"Connection error while fetching playlist: {e}")
set_server_offline("Connection failed - server unreachable")
except requests.exceptions.RequestException as e:
Logger.error(f"Failed to fetch playlist: {e}")
Logger.error(f"Request error while fetching playlist: {e}")
set_server_offline(f"Network error: {str(e)}")
except Exception as e:
Logger.error(f"Unexpected error while fetching playlist: {e}")
set_server_offline(f"Unexpected error: {str(e)}")
return {'playlist': [], 'version': 0}
def save_playlist_with_version(playlist_data, playlist_dir):
@@ -141,8 +356,11 @@ def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_d
"""
Fetch the server playlist once, compare versions, and update if needed.
Returns True if updated, False if already up to date.
Also sends feedback to server about playlist check.
"""
import json
global SERVER_CONNECTION_STATUS
server_data = fetch_server_playlist(config)
server_version = server_data.get('version', 0)
if not os.path.exists(local_playlist_path):
@@ -151,20 +369,30 @@ def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_d
with open(local_playlist_path, 'r') as f:
local_data = json.load(f)
local_version = local_data.get('version', 0)
Logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
if local_version != server_version:
# Only send feedback if server is online
if SERVER_CONNECTION_STATUS['is_online']:
send_playlist_check_feedback(config, server_version if server_version > 0 else local_version)
if local_version != server_version and server_version > 0:
if server_data and server_data.get('playlist'):
updated_playlist = download_media_files(server_data['playlist'], media_dir)
server_data['playlist'] = updated_playlist
save_playlist_with_version(server_data, playlist_dir)
# Delete old playlists and unreferenced media
delete_old_playlists_and_media(server_version, playlist_dir, media_dir)
# Update last playlist update time
SERVER_CONNECTION_STATUS['last_playlist_update'] = datetime.datetime.now()
return True
else:
Logger.warning("No playlist data fetched from server or playlist is empty.")
return False
else:
Logger.info("Local playlist is already up to date.")
Logger.info("Local playlist is already up to date or server is offline.")
return False

View File

@@ -54,9 +54,12 @@ def main():
def reload_playlist_if_updated():
new_playlist = load_latest_playlist()
if new_playlist != player.playlist:
print("[MAIN] Playlist updated, reloading...")
player.playlist = new_playlist
player.current_index = 0
player.show_current_media()
# Only restart if we're not already in a transition
if not hasattr(player, 'is_transitioning') or not player.is_transitioning:
player.show_current_media()
root.after(10000, reload_playlist_if_updated)
reload_playlist_if_updated()

View File

@@ -2,7 +2,7 @@
"screen_orientation": "Landscape",
"screen_name": "tv-terasa",
"quickconnect_key": "8887779",
"server_ip": "10.232.7.231",
"server_ip": "192.168.1.22",
"port": "80",
"screen_w": "1920",
"screen_h": "1080",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,423 @@
import os
import json
import tkinter as tk
import vlc
import subprocess
import sys
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
class SimpleTkPlayer:
def __init__(self, root, playlist):
self.root = root
self.playlist = playlist
self.current_index = 0
self.paused = False
self.pause_timer = None
self.label = tk.Label(root, bg='black')
self.label.pack(fill=tk.BOTH, expand=True)
self.create_controls()
self.hide_controls()
self.root.bind('<Motion>', self.on_activity)
self.root.bind('<Button-1>', self.on_activity)
self.root.after(100, self.ensure_fullscreen)
self.root.after(200, self.hide_mouse)
self.root.after(300, self.move_mouse_to_corner)
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
def ensure_fullscreen(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
def create_controls(self):
# Create a transparent, borderless top-level window for controls
self.controls_win = tk.Toplevel(self.root)
self.controls_win.overrideredirect(True)
self.controls_win.attributes('-topmost', True)
self.controls_win.attributes('-alpha', 0.92)
self.controls_win.configure(bg='')
# Place the window at the bottom right
def place_controls():
self.controls_win.update_idletasks()
w = self.controls_win.winfo_reqwidth()
h = self.controls_win.winfo_reqheight()
sw = self.root.winfo_screenwidth()
sh = self.root.winfo_screenheight()
x = sw - w - 30
y = sh - h - 30
self.controls_win.geometry(f'+{x}+{y}')
self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge')
self.controls_frame.pack()
btn_style = {
'bg': '#333',
'fg': 'white',
'activebackground': '#555',
'activeforeground': '#00e6e6',
'font': ('Arial', 16, 'bold'),
'bd': 0,
'highlightthickness': 0,
'relief': 'flat',
'cursor': 'hand2',
'padx': 10,
'pady': 6
}
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
self.prev_btn.grid(row=0, column=0, padx=4)
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
self.pause_btn.grid(row=0, column=1, padx=4)
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
self.next_btn.grid(row=0, column=2, padx=4)
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
self.settings_btn.grid(row=0, column=3, padx=4)
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
self.exit_btn.grid(row=0, column=4, padx=4)
self.exit_btn.config(fg='#ff4d4d')
self.controls_win.withdraw()
self.controls_win.after(200, place_controls)
self.root.bind('<Configure>', lambda e: self.controls_win.after(200, place_controls))
def hide_mouse(self):
self.root.config(cursor='none')
if hasattr(self, 'controls_win'):
self.controls_win.config(cursor='none')
def show_mouse(self):
self.root.config(cursor='arrow')
if hasattr(self, 'controls_win'):
self.controls_win.config(cursor='arrow')
def move_mouse_to_corner(self):
try:
import pyautogui
sw = self.root.winfo_screenwidth()
sh = self.root.winfo_screenheight()
pyautogui.moveTo(sw-2, sh-2)
except Exception:
pass
def show_controls(self):
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
self.create_controls()
self.controls_win.deiconify()
self.controls_win.lift()
self.show_mouse()
self.schedule_hide_controls()
def hide_controls(self):
self.controls_win.withdraw()
self.hide_mouse()
def schedule_hide_controls(self):
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
self.root.after_cancel(self.hide_controls_timer)
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
def on_activity(self, event=None):
self.show_controls()
def prev_media(self):
self.current_index = (self.current_index - 1) % len(self.playlist)
self.show_current_media()
def next_media(self):
# If at the last media, update playlist before looping
if self.current_index == len(self.playlist) - 1:
self.update_playlist_from_server()
self.current_index = 0
self.show_current_media()
else:
self.current_index = (self.current_index + 1) % len(self.playlist)
self.show_current_media()
def update_playlist_from_server(self):
# Dummy implementation: replace with your actual update logic
# For example, call a function to fetch and reload the playlist
print("[INFO] Updating playlist from server...")
# You can import and call your real update function here
# Example: self.playlist = get_latest_playlist()
def toggle_pause(self):
if not self.paused:
self.paused = True
self.pause_btn.config(text='▶ Resume')
self.pause_timer = self.root.after(30000, self.resume_play)
else:
self.resume_play()
def resume_play(self):
self.paused = False
self.pause_btn.config(text='⏸ Pause')
if self.pause_timer:
self.root.after_cancel(self.pause_timer)
self.pause_timer = None
def play_intro_video(self):
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
if os.path.exists(intro_path):
self.show_video(intro_path, on_end=self.after_intro)
else:
self.after_intro()
def after_intro(self):
self.show_current_media()
self.root.after(100, self.next_media_loop)
def show_video(self, file_path, on_end=None, duration=None):
try:
print(f"[PLAYER] Attempting to play video: {file_path}")
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if video doesn't end
def watchdog():
print(f"[WATCHDOG] Video watchdog triggered for {file_path}")
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
max_duration = duration if duration is not None else 60 # fallback max 60s
self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog)
def finish_video():
if hasattr(self, 'video_watchdog'):
self.root.after_cancel(self.video_watchdog)
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
if duration is not None:
self.root.after(int(duration * 1000), finish_video)
else:
def check_end():
try:
if self.vlc_player.get_state() == vlc.State.Ended:
finish_video()
elif self.vlc_player.get_state() == vlc.State.Error:
print(f"[VLC] Error state detected for {file_path}")
finish_video()
else:
self.root.after(200, check_end)
except Exception as e:
print(f"[VLC] Exception in check_end: {e}")
finish_video()
check_end()
except Exception as e:
print(f"[VLC] Error playing video {file_path}: {e}")
if on_end:
on_end()
def show_current_media(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
if not self.playlist:
print("[PLAYER] Playlist is empty. No media to show.")
self.label.config(text="No media available", fg='white', font=('Arial', 32))
# Try to reload playlist after 10 seconds
self.root.after(10000, self.reload_playlist_and_continue)
return
media = self.playlist[self.current_index]
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
ext = file_path.lower()
duration = media.get('duration', None)
if not os.path.isfile(file_path):
print(f"[PLAYER] File missing: {file_path}. Skipping to next.")
self.next_media()
return
if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
self.show_video(file_path, on_end=self.next_media, duration=duration)
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media)
else:
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
self.root.after(2000, self.next_media)
def reload_playlist_and_continue(self):
print("[PLAYER] Attempting to reload playlist...")
new_playlist = load_latest_playlist()
if new_playlist:
self.playlist = new_playlist
self.current_index = 0
print("[PLAYER] Playlist reloaded. Continuing playback.")
self.show_current_media()
else:
print("[PLAYER] Still no playlist. Will retry.")
self.root.after(10000, self.reload_playlist_and_continue)
def show_image_via_vlc(self, file_path, duration, on_end=None):
try:
print(f"[PLAYER] Attempting to show image: {file_path}")
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if image doesn't advance
def watchdog():
print(f"[WATCHDOG] Image watchdog triggered for {file_path}")
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
self.image_watchdog = self.root.after(int(duration * 1200), watchdog)
def finish_image():
if hasattr(self, 'image_watchdog'):
self.root.after_cancel(self.image_watchdog)
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
self.root.after(int(duration * 1000), finish_image)
except Exception as e:
print(f"[VLC] Error showing image {file_path}: {e}")
if on_end:
on_end()
def next_media(self):
self.current_index = (self.current_index + 1) % len(self.playlist)
self.show_current_media()
def next_media_loop(self):
if not self.playlist or self.paused:
self.root.after(1000, self.next_media_loop)
return
self.show_current_media()
def exit_app(self):
# Signal all threads and flags to stop
if hasattr(self, 'stop_event') and self.stop_event:
self.stop_event.set()
if hasattr(self, 'app_running') and self.app_running:
self.app_running[0] = False
# Unbind all events to prevent callbacks after destroy
try:
self.root.unbind('<Motion>')
self.root.unbind('<Button-1>')
self.root.unbind('<Configure>')
except Exception:
pass
# Attempt to destroy all Toplevel windows before root
try:
# Withdraw controls_win if it exists
if hasattr(self, 'controls_win') and self.controls_win:
if self.controls_win.winfo_exists():
self.controls_win.withdraw()
# Destroy controls_win if it exists (this will also destroy controls_frame)
if hasattr(self, 'controls_win') and self.controls_win:
if self.controls_win.winfo_exists():
self.controls_win.destroy()
self.controls_win = None
# Fallback: destroy controls_frame if it somehow still exists
if hasattr(self, 'controls_frame') and self.controls_frame:
if self.controls_frame.winfo_exists():
self.controls_frame.destroy()
self.controls_frame = None
# Fallback: destroy any remaining Toplevels in the app
for widget in self.root.winfo_children():
if isinstance(widget, tk.Toplevel):
try:
widget.destroy()
except Exception:
pass
except Exception as e:
print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}")
# Destroy any other Toplevels if needed (add here if you have more)
try:
if self.root.winfo_exists():
self.root.destroy()
except Exception as e:
print(f"[EXIT] Error destroying root: {e}")
def open_settings(self):
if self.paused is not True:
self.paused = True
self.pause_btn.config(text='▶ Resume')
# Explicitly pause VLC video if playing
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
self.vlc_player.pause()
except Exception:
pass
# Destroy controls overlay so settings window is always interactive
if hasattr(self, 'controls_win') and self.controls_win:
self.controls_win.destroy()
self.controls_win = None
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
# Open settings in a new process so it doesn't block the main player
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
# Give the window manager a moment to focus the new window
self.root.after(300, lambda: self.root.focus_force())
# Wait for the settings window to close, then resume
self.root.after(1000, lambda: self.check_settings_closed(proc))
def check_settings_closed(self, proc):
if proc.poll() is not None:
# Resume playback and unpause VLC if needed
self.resume_play()
# Restore and recreate controls overlay
self.root.deiconify()
self.create_controls()
# Re-bind mouse and button events to new controls
self.root.bind('<Motion>', self.on_activity)
self.root.bind('<Button-1>', self.on_activity)
self.show_controls()
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
# Only resume if it was paused by us
if self.vlc_player.get_state() == vlc.State.Paused:
self.vlc_player.play()
except Exception:
pass
else:
self.root.after(1000, lambda: self.check_settings_closed(proc))
def main_start(self):
self.play_intro_video()
def load_latest_playlist():
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
if not files:
return []
# Sort by version number descending
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
latest_file = files[0]
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
data = json.load(f)
playlist = data.get('playlist', [])
# Validate playlist: skip missing or unsupported files
valid_exts = ('.mp4', '.avi', '.mov', '.mkv', '.jpg', '.jpeg', '.png', '.bmp', '.gif')
valid_playlist = []
for item in playlist:
file_path = os.path.join(MEDIA_DATA_PATH, item.get('file_name', ''))
if os.path.isfile(file_path) and file_path.lower().endswith(valid_exts):
valid_playlist.append(item)
else:
print(f"[PLAYLIST] Skipping missing or unsupported file: {item.get('file_name')}")
return valid_playlist

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,20 +1,20 @@
{
"playlist": [
{
"file_name": "HARTING_Safety_day_informare_2_page_003.jpg",
"url": "media/HARTING_Safety_day_informare_2_page_003.jpg",
"file_name": "one-piece-season-2-5120x2880-23673.jpg",
"url": "media/one-piece-season-2-5120x2880-23673.jpg",
"duration": 30
},
{
"file_name": "call-of-duty-black-3840x2160-23674.jpg",
"url": "media/call-of-duty-black-3840x2160-23674.jpg",
"duration": 30
},
{
"file_name": "big-buck-bunny-1080p-60fps-30sec.mp4",
"url": "media/big-buck-bunny-1080p-60fps-30sec.mp4",
"duration": 30
},
{
"file_name": "one-piece-season-2-5120x2880-23673.jpg",
"url": "media/one-piece-season-2-5120x2880-23673.jpg",
"duration": 30
}
],
"version": 6
"version": 8
}

81
troubleshoot_32bit.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/bash
# Troubleshooting script for 32-bit bcrypt issues
echo "🔍 32-BIT SYSTEM TROUBLESHOOTING"
echo "================================"
# System info
echo "1. System Information:"
echo " Architecture: $(uname -m)"
echo " OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
echo " Python: $(python3 --version)"
echo ""
# Check if we're in virtual environment
echo "2. Environment Check:"
if [ -n "$VIRTUAL_ENV" ]; then
echo " ✅ In virtual environment: $VIRTUAL_ENV"
echo " Python location: $(which python)"
echo " Pip location: $(which pip)"
else
echo " ❌ Not in virtual environment"
echo " System Python: $(which python3)"
fi
echo ""
# Check bcrypt installation
echo "3. Bcrypt Investigation:"
if [ -n "$VIRTUAL_ENV" ]; then
echo " Checking installed bcrypt..."
pip list | grep bcrypt || echo " ❌ bcrypt not installed"
echo " Testing bcrypt import..."
python3 -c "
try:
import bcrypt
print(' ✅ bcrypt imports successfully')
# Check bcrypt module location
print(f' Module location: {bcrypt.__file__}')
# Test basic functionality
test_pw = b'test123'
hashed = bcrypt.hashpw(test_pw, bcrypt.gensalt())
if bcrypt.checkpw(test_pw, hashed):
print(' ✅ bcrypt functionality works')
else:
print(' ❌ bcrypt functionality failed')
except ImportError as e:
print(f' ❌ bcrypt import failed: {e}')
except Exception as e:
print(f' ❌ bcrypt error: {e}')
"
else
echo " ⚠️ Please activate virtual environment first"
fi
echo ""
echo "4. Library Files Check:"
if [ -d "req_libraries_32bit" ]; then
echo " ✅ req_libraries_32bit exists"
if ls req_libraries_32bit/bcrypt*.whl >/dev/null 2>&1; then
BCRYPT_FILE=$(ls req_libraries_32bit/bcrypt*.whl | head -1)
echo " 32-bit bcrypt: $(basename $BCRYPT_FILE)"
# Quick check of wheel architecture
echo " Checking wheel architecture..."
unzip -l "$BCRYPT_FILE" | grep "\.so" | head -1 || echo " No .so files found"
else
echo " ❌ No bcrypt wheel found in 32-bit libraries"
fi
else
echo " ❌ req_libraries_32bit folder missing"
fi
echo ""
echo "5. Recommendations:"
echo " • Ensure you're on 32-bit Raspberry Pi OS (armv7l)"
echo " • Use: source .venv/bin/activate"
echo " • Run: ./install_32bit.sh for clean installation"
echo " • If still failing, remove .venv and reinstall"

View File

@@ -1,11 +0,0 @@
Video Profile for Player Compatibility (based on intro1.mp4)
Codec: H.264
Profile: Main
Resolution: 1920x1080
Bitrate: ~14,700 kbps
Framerate: 29.97 fps
Recommended ffmpeg conversion command:
ffmpeg -i input.mp4 -c:v libx264 -profile:v main -b:v 14700k -vf "scale=1920:1080,fps=29.97" -c:a copy output_normalized.mp4