Compare commits

...

31 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
2a564f5e84 Improve robustness: add watchdog timers, error handling, and logging to prevent player freeze 2025-08-29 13:23:40 +03:00
8d69a737f9 deleted old app 2025-08-26 16:18:28 +03:00
22901043b7 Playlist logic fixes, always use latest playlist version, VLC-only image display, and bugfixes 2025-08-26 16:14:53 +03:00
f4c73b54f7 Save all current changes: video profile, player fixes, and compatibility improvements 2025-08-25 21:48:58 +03:00
f77f717af9 updating settings 2025-08-25 19:55:40 +03:00
0c162bb0c8 UI/UX: Improved mouse hiding logic, overlay controls, and robust player integration 2025-08-25 16:37:54 +03:00
1ea2ee584c Stable: Fullscreen player, intro video, playlist update, overlay controls, clean shutdown, mouse hide, and robust exit fixes 2025-08-25 15:30:37 +03:00
027709618e aaaabbbb 2025-08-24 22:30:51 +03:00
8f64de240b further to check : showing of the meniu on player ui and player ui to run in fullscreen 2025-08-24 00:07:53 +03:00
da91677f5b final touches 2025-08-23 23:55:20 +03:00
1579371395 removed old bloat 2025-08-23 19:04:26 +03:00
99338b88c7 final; 2025-08-23 19:03:10 +03:00
751a781634 Clean up and simplify settings screen connection tab; remove duplicate/test code and fix UI issues 2025-08-23 17:45:03 +03:00
e2afbee438 Final project update: all fixes, endpoint corrections, and production-ready state 2025-08-23 16:13:47 +03:00
116 changed files with 24662 additions and 7319 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
venv/
venv/
tkinter_app/

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.

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
python-vlc
Pillow
pyautogui
requests
bcrypt
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 tkinter_app/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

Binary file not shown.

View File

@@ -0,0 +1,112 @@
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
class AppSettingsWindow(tk.Tk):
def __init__(self):
super().__init__()
self.title('App Settings')
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'))
self.style.configure('TEntry', fieldbackground='#2c313c', foreground='#e0e0e0', borderwidth=1, relief='flat', font=('Segoe UI', 12))
self.style.map('TEntry', fieldbackground=[('active', '#23272e')])
self.style.configure('TButton', background='#3b82f6', foreground='white', font=('Segoe UI', 13, 'bold'), borderwidth=0, focusthickness=3, focuscolor='#60a5fa', padding=8)
self.style.map('TButton', background=[('active', '#2563eb')])
def load_config(self):
try:
with open(CONFIG_PATH, 'r') as f:
self.config_data = json.load(f)
except Exception as e:
self.config_data = {}
messagebox.showerror('Error', f'Failed to load config: {e}')
def save_config(self):
try:
for key, entry in self.fields.items():
if hasattr(entry, 'get'):
self.config_data[key] = entry.get()
with open(CONFIG_PATH, 'w') as f:
json.dump(self.config_data, f, indent=4)
self.show_custom_popup('Success', 'Settings saved!')
except Exception as e:
messagebox.showerror('Error', f'Failed to save config: {e}')
def show_custom_popup(self, title, message):
popup = tk.Toplevel(self)
popup.title(title)
popup.geometry('320x120')
popup.configure(bg='#23272e')
popup.resizable(False, False)
popup.attributes('-topmost', True)
ttk.Label(popup, text=title, style='TLabel').pack(pady=(18, 0))
ttk.Label(popup, text=message, style='TLabel').pack(pady=(8, 0))
close_btn = ttk.Button(popup, text='OK', style='TButton', command=popup.destroy)
close_btn.pack(pady=18)
popup.grab_set()
popup.after(5000, popup.destroy)
def create_widgets(self):
title = ttk.Label(self, text='Application Settings', style='TLabel')
title.grid(row=0, column=0, columnspan=2, pady=(18, 6))
warning = tk.Label(self, text='⚠️ Modify values only if necessary or instructed!', fg='#ffb300', bg='#23272e', font=('Segoe UI', 11, 'bold'))
warning.grid(row=1, column=0, columnspan=2, pady=(0, 18))
row = 2
for key, value in self.config_data.items():
label = ttk.Label(self, text=key+':', style='TLabel')
label.grid(row=row, column=0, sticky='e', padx=18, pady=10)
if key == 'touch_screen':
var = tk.StringVar()
combo = ttk.Combobox(self, textvariable=var, values=['On', 'Off'], state='readonly', width=26)
combo.set(str(value))
combo.grid(row=row, column=1, padx=10, pady=10)
self.fields[key] = combo
else:
entry = ttk.Entry(self, style='TEntry', width=28)
entry.insert(0, str(value))
entry.grid(row=row, column=1, padx=10, pady=10)
self.fields[key] = entry
row += 1
save_btn = ttk.Button(self, text='Save Settings', style='TButton', command=self.save_config)
save_btn.grid(row=row, column=0, pady=30, sticky='e', padx=(0,10))
close_btn = ttk.Button(self, text='Close', style='TButton', command=self.destroy)
close_btn.grid(row=row, column=1, pady=30, sticky='w', padx=(10,0))
if __name__ == '__main__':
app = AppSettingsWindow()
app.mainloop()

View File

@@ -0,0 +1,399 @@
import os
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.
Returns True if up-to-date, False otherwise.
"""
import json
if not os.path.exists(local_playlist_path):
Logger.info(f"Local playlist file not found: {local_playlist_path}")
return False
with open(local_playlist_path, 'r') as f:
local_data = json.load(f)
local_version = local_data.get('version', 0)
server_data = fetch_server_playlist(config)
server_version = server_data.get('version', 0)
Logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
return local_version == server_version
def fetch_server_playlist(config):
"""Fetch the updated playlist from the server using a config dict."""
server = config.get("server_ip", "")
host = config.get("screen_name", "")
quick = config.get("quickconnect_key", "")
port = config.get("port", "")
try:
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
if re.match(ip_pattern, server):
server_url = f'http://{server}:{port}/api/playlists'
else:
server_url = f'http://{server}/api/playlists'
params = {
'hostname': host,
'quickconnect_code': quick
}
Logger.info(f"Fetching playlist from URL: {server_url} with 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"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):
version = playlist_data.get('version', 0)
playlist_file = os.path.join(playlist_dir, f'server_playlist_v{version}.json')
with open(playlist_file, 'w') as f:
json.dump(playlist_data, f, indent=2)
print(f"Playlist saved to {playlist_file}")
return playlist_file
def download_media_files(playlist, media_dir):
"""Download media files from the server and save them to media_dir."""
if not os.path.exists(media_dir):
os.makedirs(media_dir)
Logger.info(f"Created directory {media_dir} for media files.")
updated_playlist = []
for media in playlist:
file_name = media.get('file_name', '')
file_url = media.get('url', '')
duration = media.get('duration', 10)
local_path = os.path.join(media_dir, file_name)
Logger.info(f"Preparing to download {file_name} from {file_url}...")
if os.path.exists(local_path):
Logger.info(f"File {file_name} already exists. Skipping download.")
else:
try:
response = requests.get(file_url, timeout=10)
if response.status_code == 200:
with open(local_path, 'wb') as file:
file.write(response.content)
Logger.info(f"Successfully downloaded {file_name} to {local_path}")
else:
Logger.error(f"Failed to download {file_name}. Status Code: {response.status_code}")
continue
except requests.exceptions.RequestException as e:
Logger.error(f"Error downloading {file_name}: {e}")
continue
updated_media = {
'file_name': file_name,
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
'duration': duration
}
updated_playlist.append(updated_media)
return updated_playlist
def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, keep_versions=1):
"""
Delete old playlist files and media files not referenced by the latest playlist version.
keep_versions: number of latest versions to keep (default 1)
"""
# Find all playlist files
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
# Keep only the latest N versions
versions = sorted([int(f.split('_v')[-1].split('.json')[0]) for f in playlist_files], reverse=True)
keep = set(versions[:keep_versions])
# Delete old playlist files
for f in playlist_files:
v = int(f.split('_v')[-1].split('.json')[0])
if v not in keep:
os.remove(os.path.join(playlist_dir, f))
# Collect all media files referenced by the kept playlists
referenced = set()
for v in keep:
path = os.path.join(playlist_dir, f'server_playlist_v{v}.json')
if os.path.exists(path):
with open(path, 'r') as f:
data = json.load(f)
for item in data.get('playlist', []):
referenced.add(item.get('file_name'))
# Delete media files not referenced
for f in os.listdir(media_dir):
if f not in referenced:
try:
os.remove(os.path.join(media_dir, f))
except Exception as e:
Logger.warning(f"Failed to delete media file {f}: {e}")
def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir):
"""
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):
local_version = 0
else:
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}")
# 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 or server is offline.")
return False

View File

@@ -0,0 +1,19 @@
import logging
import os
# Path to the log file (in main_data for new app)
LOG_FILE_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'log.txt')
Logger = logging.getLogger('SignageApp')
Logger.setLevel(logging.INFO)
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a')
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')
file_handler.setFormatter(formatter)
Logger.addHandler(file_handler)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(formatter)
Logger.addHandler(stream_handler)

75
signage_player/main.py Normal file
View File

@@ -0,0 +1,75 @@
import threading
import time
import os
import json
from get_playlists import update_playlist_if_needed
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
LOCAL_PLAYLIST_PATH = os.path.join(PLAYLIST_DIR, 'server_playlist.json')
def playlist_update_loop(refresh_time, app_running, config, local_playlist_path, media_dir, playlist_dir):
while app_running[0]:
updated = update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir)
if updated:
print(f"[REFRESH] Playlist updated from server at {time.strftime('%X')}")
else:
print(f"[REFRESH] Playlist already up to date at {time.strftime('%X')}")
time.sleep(refresh_time)
import tkinter as tk
from player import SimpleTkPlayer, load_latest_playlist
def main():
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
refresh_time = int(config.get('refresh_time', 5)) * 60 # minutes
app_running = [True]
update_thread = threading.Thread(
target=playlist_update_loop,
args=(refresh_time, app_running, config, LOCAL_PLAYLIST_PATH, MEDIA_DATA_PATH, PLAYLIST_DIR),
daemon=True
)
update_thread.start()
root = tk.Tk()
root.title("Simple Tkinter Player")
root.configure(bg='black')
root.attributes('-fullscreen', True)
# Load playlist and create player
playlist = load_latest_playlist()
player = SimpleTkPlayer(root, playlist)
player.app_running = app_running
orig_exit_app = player.exit_app
def exit_and_stop():
app_running[0] = False
orig_exit_app()
player.exit_app = exit_and_stop
player.exit_btn.config(command=player.exit_app)
player.main_start()
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
# 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()
try:
root.mainloop()
except KeyboardInterrupt:
pass
finally:
app_running[0] = False
# Do not join the update_thread; let it exit as a daemon
if __name__ == '__main__':
main()

View File

@@ -2,9 +2,10 @@
"screen_orientation": "Landscape",
"screen_name": "tv-terasa",
"quickconnect_key": "8887779",
"server_ip": "digi-signage.moto-adv.com",
"port": "8880",
"server_ip": "192.168.1.22",
"port": "80",
"screen_w": "1920",
"screen_h": "1080",
"playlist_version": 29
"refresh_time": "5",
"touch_screen": "On"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,353 @@
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):
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()
# Force video to play for the specified duration
def finish_video():
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():
if self.vlc_player.get_state() == vlc.State.Ended:
finish_video()
else:
self.root.after(200, check_end)
check_end()
def show_current_media(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
if not self.playlist:
self.label.config(text="No media available", fg='white', font=('Arial', 32))
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 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:
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
def show_image_via_vlc(self, file_path, duration, on_end=None):
# Use VLC to show image for a set duration
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()
# Schedule stop and next after duration
def finish_image():
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)
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)
return data.get('playlist', [])

View File

@@ -0,0 +1,224 @@
import os
import json
import tkinter as tk
from PIL import Image, ImageTk
import vlc
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)
def ensure_fullscreen(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
def create_controls(self):
self.controls_frame = tk.Frame(self.root, bg='#222', bd=2, relief='ridge')
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
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')
def show_controls(self):
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
self.controls_frame.lift()
self.schedule_hide_controls()
def hide_controls(self):
self.controls_frame.place_forget()
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):
self.current_index = (self.current_index + 1) % len(self.playlist)
self.show_current_media()
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):
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()
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()
def check_end():
if self.vlc_player.get_state() == vlc.State.Ended:
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
else:
self.root.after(200, check_end)
check_end()
def show_current_media(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
if not self.playlist:
self.label.config(text="No media available", fg='white', font=('Arial', 32))
return
media = self.playlist[self.current_index]
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
if file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
self.show_video(file_path, on_end=self.next_media)
elif file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
try:
img = Image.open(file_path)
# Fit to screen without crop or stretch
screen_w = self.root.winfo_screenwidth()
screen_h = self.root.winfo_screenheight()
img_w, img_h = img.size
scale = min(screen_w / img_w, screen_h / img_h)
new_w = int(img_w * scale)
new_h = int(img_h * scale)
img = img.resize((new_w, new_h), Image.LANCZOS)
bg = Image.new('RGB', (screen_w, screen_h), 'black')
x = (screen_w - new_w) // 2
y = (screen_h - new_h) // 2
bg.paste(img, (x, y))
photo = ImageTk.PhotoImage(bg)
self.label.config(image=photo, text='')
self.label.image = photo
except Exception as e:
self.label.config(text=f"Image error: {e}", fg='red')
else:
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
def next_media_loop(self):
if not self.playlist or self.paused:
self.root.after(1000, self.next_media_loop)
return
duration = self.playlist[self.current_index].get('duration', 10)
self.current_index = (self.current_index + 1) % len(self.playlist)
self.show_current_media()
self.root.after(duration * 1000, self.next_media_loop)
def exit_app(self):
# Signal the update thread to stop if stop_event is present
if hasattr(self, 'stop_event') and self.stop_event:
self.stop_event.set()
self.root.destroy()
def open_settings(self):
if self.paused is not True:
self.paused = True
self.pause_btn.config(text='▶ Resume')
settings_win = tk.Toplevel(self.root)
settings_win.title('Settings')
settings_win.geometry('400x300+100+100')
settings_win.transient(self.root)
settings_win.grab_set()
tk.Label(settings_win, text='Settings', font=('Arial', 18)).pack(pady=10)
# Example setting: close button
tk.Button(settings_win, text='Close', command=settings_win.destroy).pack(pady=20)
def on_close():
settings_win.grab_release()
settings_win.destroy()
self.resume_play()
settings_win.protocol('WM_DELETE_WINDOW', on_close)
settings_win.bind('<Destroy>', lambda e: self.resume_play() if not settings_win.winfo_exists() else None)
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 []
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
with open(os.path.join(PLAYLIST_DIR, files[0]), 'r') as f:
data = json.load(f)
return data.get('playlist', [])
def main():
root = tk.Tk()
root.title("Simple Tkinter Player")
root.configure(bg='black')
root.attributes('-fullscreen', True)
playlist = load_latest_playlist()
player = SimpleTkPlayer(root, playlist)
player.main_start()
root.mainloop()
if __name__ == '__main__':
main()

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

1220
signage_player/player.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

View File

@@ -0,0 +1,20 @@
{
"playlist": [
{
"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
}
],
"version": 8
}

View File

@@ -1,111 +0,0 @@
# Signage Player - Complete Offline Installation Package
This directory contains everything needed to install the Signage Player application completely offline on a Raspberry Pi or similar Debian-based system.
## Directory Structure
```
src/
├── offline_packages/ # Pre-downloaded Python packages (.whl files)
│ ├── requests-2.32.4-py3-none-any.whl
│ ├── pillow-11.1.0-cp311-cp311-linux_armv7l.whl
│ ├── pygame-2.6.1-cp311-cp311-linux_armv7l.whl
│ └── ... (dependencies)
├── shared_modules/ # Shared Python modules
│ ├── logging_config.py
│ └── python_functions.py
├── system_packages/ # System dependency information
│ └── apt_packages.txt # List of required APT packages
└── scripts/ # Installation and utility scripts
├── install_offline.sh # Main offline installation script
└── check_dependencies.sh # Dependency verification script
```
## Quick Installation
1. **Run the offline installer:**
```bash
cd /path/to/signage-player
chmod +x src/scripts/install_offline.sh
./src/scripts/install_offline.sh
```
2. **Verify installation:**
```bash
chmod +x src/scripts/check_dependencies.sh
./src/scripts/check_dependencies.sh
```
3. **Run the application:**
```bash
./run_tkinter_debug.sh
```
## What Gets Installed
### System Packages (via APT)
- Python 3 development tools
- OpenCV libraries and Python bindings
- SDL2 libraries for pygame
- Image processing libraries (JPEG, PNG, TIFF, WebP)
- Audio libraries
- Build tools
### Python Packages (from offline wheels)
- **requests** - HTTP library for server communication
- **pillow** - Image processing library
- **pygame** - Audio and input handling
- **certifi, charset_normalizer, idna, urllib3** - Dependencies
### Application Components
- Shared logging and playlist management modules
- Modern tkinter-based media player
- Configuration management
- Resource directories
## Manual Installation Steps
If you prefer to install manually:
1. **Install system packages:**
```bash
sudo apt update
cat src/system_packages/apt_packages.txt | grep -v '^#' | xargs sudo apt install -y
```
2. **Create virtual environment:**
```bash
python3 -m venv venv
source venv/bin/activate
```
3. **Install Python packages:**
```bash
pip install --no-index --find-links src/offline_packages requests pillow pygame
```
4. **Copy shared modules:**
```bash
cp src/shared_modules/*.py tkinter_app/src/
```
## Troubleshooting
- **Permission errors:** Make sure scripts are executable with `chmod +x`
- **Missing packages:** Run `src/scripts/check_dependencies.sh` to verify installation
- **Virtual environment issues:** Delete `venv` folder and re-run installer
- **OpenCV errors:** Ensure `python3-opencv` system package is installed
## Requirements
- Debian/Ubuntu-based system (Raspberry Pi OS recommended)
- Internet connection for system package installation (APT only)
- Sudo privileges for system package installation
- At least 200MB free space
## Notes
- This package includes all Python dependencies as pre-compiled wheels
- No internet connection needed for Python packages during installation
- Compatible with ARM-based systems (Raspberry Pi)
- Includes fallback mechanisms for offline operation

View File

@@ -1,77 +0,0 @@
#!/bin/bash
# Dependency Verification Script
# Checks if all required dependencies are properly installed
echo "=== Signage Player Dependency Check ==="
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
ERRORS=0
# Function to check system package
check_system_package() {
if dpkg -l | grep -q "^ii.*$1"; then
echo -e "${GREEN}${NC} $1 is installed"
else
echo -e "${RED}${NC} $1 is NOT installed"
((ERRORS++))
fi
}
# Function to check Python package
check_python_package() {
if python3 -c "import $1" 2>/dev/null; then
echo -e "${GREEN}${NC} Python package '$1' is available"
else
echo -e "${RED}${NC} Python package '$1' is NOT available"
((ERRORS++))
fi
}
echo "Checking system packages..."
check_system_package "python3-dev"
check_system_package "python3-opencv"
check_system_package "libsdl2-dev"
check_system_package "libjpeg-dev"
echo ""
echo "Checking Python packages..."
check_python_package "cv2"
check_python_package "pygame"
check_python_package "PIL"
check_python_package "requests"
echo ""
echo "Checking application files..."
if [ -f "tkinter_app/src/main.py" ]; then
echo -e "${GREEN}${NC} Main application file exists"
else
echo -e "${RED}${NC} Main application file missing"
((ERRORS++))
fi
if [ -f "tkinter_app/src/tkinter_simple_player.py" ]; then
echo -e "${GREEN}${NC} Player module exists"
else
echo -e "${RED}${NC} Player module missing"
((ERRORS++))
fi
if [ -d "venv" ]; then
echo -e "${GREEN}${NC} Virtual environment exists"
else
echo -e "${YELLOW}${NC} Virtual environment not found"
fi
echo ""
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}=== All dependencies are properly installed! ===${NC}"
echo "You can run the application with: ./run_tkinter_debug.sh"
else
echo -e "${RED}=== Found $ERRORS issues ===${NC}"
echo "Run the installation script: src/scripts/install_offline.sh"
fi

View File

@@ -1,109 +0,0 @@
#!/bin/bash
# Offline Installation Script for Signage Player
# This script installs all dependencies and sets up the application completely offline
set -e # Exit on any error
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
echo "=== Signage Player Offline Installation ==="
echo "Project root: $PROJECT_ROOT"
# Check if running as root for system packages
if [[ $EUID -eq 0 ]]; then
echo "ERROR: Please run this script as a regular user, not as root."
echo "The script will prompt for sudo when needed."
exit 1
fi
# Function to check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Install system packages
echo "Step 1: Installing system packages..."
if command_exists apt; then
echo "Installing APT packages..."
sudo apt update
# Read and install packages from the list
while IFS= read -r package; do
# Skip comments and empty lines
if [[ ! "$package" =~ ^[[:space:]]*# ]] && [[ -n "${package// }" ]]; then
echo "Installing: $package"
sudo apt install -y "$package" || echo "Warning: Could not install $package"
fi
done < "$SCRIPT_DIR/../system_packages/apt_packages.txt"
else
echo "ERROR: apt package manager not found. This script is designed for Debian/Ubuntu systems."
exit 1
fi
# Create virtual environment
echo "Step 2: Creating virtual environment..."
cd "$PROJECT_ROOT"
if [ ! -d "venv" ]; then
python3 -m venv --system-site-packages venv
echo "Virtual environment created with system site packages access."
else
echo "Removing existing virtual environment and creating new one with system site packages..."
rm -rf venv
python3 -m venv --system-site-packages venv
echo "Virtual environment recreated with system site packages access."
fi
# Activate virtual environment
echo "Step 3: Activating virtual environment..."
source venv/bin/activate
# Install Python packages from offline wheels
echo "Step 4: Installing Python packages from offline wheels..."
OFFLINE_PACKAGES_DIR="$PROJECT_ROOT/src/offline_packages"
if [ -d "$OFFLINE_PACKAGES_DIR" ]; then
pip install --upgrade pip
pip install --no-index --find-links "$OFFLINE_PACKAGES_DIR" \
requests pillow pygame certifi charset_normalizer idna urllib3
echo "Python packages installed successfully."
else
echo "ERROR: Offline packages directory not found at: $OFFLINE_PACKAGES_DIR"
exit 1
fi
# Copy shared modules to tkinter app
echo "Step 5: Setting up shared modules..."
SHARED_MODULES_DIR="$PROJECT_ROOT/src/shared_modules"
TKINTER_APP_DIR="$PROJECT_ROOT/tkinter_app/src"
if [ -d "$SHARED_MODULES_DIR" ]; then
cp "$SHARED_MODULES_DIR"/*.py "$TKINTER_APP_DIR"/ 2>/dev/null || echo "Shared modules already in place."
echo "Shared modules configured."
else
echo "Warning: Shared modules directory not found at: $SHARED_MODULES_DIR"
fi
# Create necessary directories
echo "Step 6: Creating application directories..."
mkdir -p "$PROJECT_ROOT/tkinter_app/resources/static/resurse"
mkdir -p "$PROJECT_ROOT/tkinter_app/src/static/resurse"
# Set permissions
echo "Step 7: Setting permissions..."
chmod +x "$PROJECT_ROOT/run_tkinter_debug.sh" 2>/dev/null || true
chmod +x "$PROJECT_ROOT/install_tkinter.sh" 2>/dev/null || true
chmod +x "$PROJECT_ROOT/src/scripts"/*.sh 2>/dev/null || true
echo ""
echo "=== Installation Complete! ==="
echo ""
echo "To run the signage player:"
echo " ./run_tkinter_debug.sh"
echo ""
echo "To configure settings, run the app and click the Settings button."
echo ""
echo "The application has been set up with:"
echo " - All system dependencies"
echo " - Python virtual environment with required packages"
echo " - Shared modules properly configured"
echo " - Necessary directories created"
echo ""

View File

@@ -1,27 +0,0 @@
import logging
import os
# Path to the log file
# Update the path to point to the new resources directory
LOG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
# Create a logger instance
Logger = logging.getLogger('SignageApp')
Logger.setLevel(logging.INFO) # Set the logging level to INFO
# Create a file handler to write logs to the log.txt file
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a') # Append logs to the file
file_handler.setLevel(logging.INFO)
# Create a formatter for the log messages
formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')
file_handler.setFormatter(formatter)
# Add the file handler to the logger
Logger.addHandler(file_handler)
# Optionally, add a stream handler to log messages to the console
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(formatter)
Logger.addHandler(stream_handler)

View File

@@ -1,196 +0,0 @@
import os
import json
import requests
from logging_config import Logger # Import the shared logger
import bcrypt
import time
# Update paths to use the new directory structure
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
LOCAL_PLAYLIST_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'local_playlist.json')
def load_config():
"""Load configuration from app_config.txt."""
Logger.info("python_functions: Starting load_config function.")
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r') as file:
Logger.info("python_functions: Configuration file loaded successfully.")
return json.load(file)
except json.JSONDecodeError as e:
Logger.error(f"python_functions: Failed to parse configuration file. Error: {e}")
return {}
else:
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
return {}
Logger.info("python_functions: Finished load_config function.")
# Load configuration and initialize variables
config_data = load_config()
server = config_data.get("server_ip", "")
host = config_data.get("screen_name", "")
quick = config_data.get("quickconnect_key", "")
port = config_data.get("port", "")
Logger.info(f"python_functions: Configuration loaded: server={server}, host={host}, quick={quick}, port={port}")
def load_local_playlist():
"""Load the playlist and version from local storage."""
Logger.info("python_functions: Starting load_local_playlist function.")
if os.path.exists(LOCAL_PLAYLIST_FILE):
try:
with open(LOCAL_PLAYLIST_FILE, 'r') as local_file:
local_playlist = json.load(local_file)
Logger.info(f"python_functions: Local playlist loaded: {local_playlist}")
if isinstance(local_playlist, dict) and 'playlist' in local_playlist and 'version' in local_playlist:
Logger.info("python_functions: Finished load_local_playlist function successfully.")
return local_playlist # Return the full playlist data
else:
Logger.error("python_functions: Invalid local playlist structure.")
return {'playlist': [], 'version': 0}
except json.JSONDecodeError as e:
Logger.error(f"python_functions: Failed to parse local playlist file. Error: {e}")
return {'playlist': [], 'version': 0}
else:
Logger.warning("python_functions: Local playlist file not found.")
return {'playlist': [], 'version': 0}
Logger.info("python_functions: Finished load_local_playlist function.")
def save_local_playlist(playlist):
"""Save the updated playlist locally."""
Logger.info("python_functions: Starting save_local_playlist function.")
Logger.debug(f"python_functions: Playlist to save: {playlist}")
if not playlist or 'playlist' not in playlist:
Logger.error("python_functions: Invalid playlist data. Cannot save local playlist.")
return
try:
with open(LOCAL_PLAYLIST_FILE, 'w') as local_file:
json.dump(playlist, local_file, indent=4) # Ensure proper formatting
Logger.info("python_functions: Updated local playlist with server data.")
except IOError as e:
Logger.error(f"python_functions: Failed to save local playlist: {e}")
Logger.info("python_functions: Finished save_local_playlist function.")
def fetch_server_playlist():
"""Fetch the updated playlist from the server."""
try:
server_ip = f'{server}:{port}' # Construct the server IP with port
url = f'http://{server_ip}/api/playlists'
params = {
'hostname': host,
'quickconnect_code': quick
}
Logger.info(f"Fetching playlist from URL: {url} with params: {params}")
response = requests.get(url, params=params)
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.")
# Update the playlist version in app_config.txt
update_config_playlist_version(version)
return {'playlist': playlist, 'version': version}
else:
Logger.error("Quickconnect code validation failed.")
else:
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
else:
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
except requests.exceptions.RequestException as e:
Logger.error(f"Failed to fetch playlist: {e}")
return {'playlist': [], 'version': 0}
def download_media_files(playlist, version):
"""Download media files from the server and update the local playlist."""
Logger.info("python_functions: Starting media file download...")
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') # Path to the local folder
if not os.path.exists(base_dir):
os.makedirs(base_dir)
Logger.info(f"python_functions: Created directory {base_dir} for media files.")
updated_playlist = [] # List to store updated media entries
for media in playlist:
file_name = media.get('file_name', '')
file_url = media.get('url', '')
duration = media.get('duration', 10) # Default duration if not provided
local_path = os.path.join(base_dir, file_name) # Local file path
Logger.debug(f"python_functions: Preparing to download {file_name} from {file_url}...")
if os.path.exists(local_path):
Logger.info(f"python_functions: File {file_name} already exists. Skipping download.")
else:
try:
response = requests.get(file_url, timeout=10)
if response.status_code == 200:
with open(local_path, 'wb') as file:
file.write(response.content)
Logger.info(f"python_functions: Successfully downloaded {file_name} to {local_path}")
else:
Logger.error(f"python_functions: Failed to download {file_name}. Status Code: {response.status_code}")
continue
except requests.exceptions.RequestException as e:
Logger.error(f"python_functions: Error downloading {file_name}: {e}")
continue
# Update the playlist entry to point to the local file path
updated_media = {
'file_name': file_name,
'url': f"static/resurse/{file_name}", # Update URL to local path
'duration': duration
}
Logger.debug(f"python_functions: Updated media entry: {updated_media}")
updated_playlist.append(updated_media)
# Save the updated playlist locally
save_local_playlist({'playlist': updated_playlist, 'version': version})
Logger.info("python_functions: Finished media file download and updated local playlist.")
def clean_unused_files(playlist):
"""Remove unused media files from the resource folder."""
Logger.info("python_functions: Cleaning unused media files...")
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
if not os.path.exists(base_dir):
Logger.debug(f"python_functions: Directory {base_dir} does not exist. No files to clean.")
return
playlist_files = {media.get('file_name', '') for media in playlist}
all_files = set(os.listdir(base_dir))
unused_files = all_files - playlist_files
for file_name in unused_files:
file_path = os.path.join(base_dir, file_name)
try:
os.remove(file_path)
Logger.info(f"python_functions: Deleted unused file: {file_path}")
except OSError as e:
Logger.error(f"python_functions: Failed to delete {file_path}: {e}")
def update_config_playlist_version(version):
"""Update the playlist version in app_config.txt."""
if not os.path.exists(CONFIG_FILE):
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
return
try:
with open(CONFIG_FILE, 'r') as file:
config_data = json.load(file)
config_data['playlist_version'] = version # Add or update the playlist version
with open(CONFIG_FILE, 'w') as file:
json.dump(config_data, file, indent=4)
Logger.info(f"python_functions: Updated playlist version in app_config.txt to {version}.")
except (IOError, json.JSONDecodeError) as e:
Logger.error(f"python_functions: Failed to update playlist version in app_config.txt. Error: {e}")

View File

@@ -1,43 +0,0 @@
#!/bin/bash
# System Package Dependencies for Signage Player
# These packages need to be installed via apt before installing Python packages
# Core system packages
python3-dev
python3-pip
python3-venv
python3-setuptools
python3-wheel
# OpenCV system dependencies
libopencv-dev
python3-opencv
libopencv-core-dev
libopencv-imgproc-dev
libopencv-imgcodecs-dev
libopencv-videoio-dev
# Audio/Video libraries
libasound2-dev
libsdl2-dev
libsdl2-image-dev
libsdl2-mixer-dev
libsdl2-ttf-dev
libfreetype6-dev
libportmidi-dev
# Image processing libraries
libjpeg-dev
libpng-dev
libtiff-dev
libwebp-dev
libopenjp2-7-dev
# Build tools (may be needed for some packages)
build-essential
cmake
pkg-config
# Networking
curl
wget

View File

@@ -1,143 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify window centering functionality
"""
import tkinter as tk
import sys
import os
sys.path.append('tkinter_app/src')
from tkinter_simple_player import SettingsWindow, SimpleMediaPlayerApp
def test_settings_centering():
"""Test settings window centering"""
root = tk.Tk()
root.withdraw() # Hide main window
# Create a mock app object
class MockApp:
def __init__(self):
self.playlist = []
self.current_index = 0
def play_current_media(self):
print('play_current_media called')
app = MockApp()
# Test settings window centering
try:
print("Testing settings window centering...")
settings = SettingsWindow(root, app)
# Get screen dimensions
screen_width = settings.window.winfo_screenwidth()
screen_height = settings.window.winfo_screenheight()
# Get window position
settings.window.update_idletasks()
window_x = settings.window.winfo_x()
window_y = settings.window.winfo_y()
window_width = 900
window_height = 700
# Calculate expected center position
expected_x = (screen_width - window_width) // 2
expected_y = (screen_height - window_height) // 2
print(f"Screen size: {screen_width}x{screen_height}")
print(f"Window position: {window_x}, {window_y}")
print(f"Expected center: {expected_x}, {expected_y}")
print(f"Window size: {window_width}x{window_height}")
# Check if window is roughly centered (allow some margin for window decorations)
margin = 50
is_centered_x = abs(window_x - expected_x) <= margin
is_centered_y = abs(window_y - expected_y) <= margin
if is_centered_x and is_centered_y:
print("✅ Settings window is properly centered!")
else:
print("❌ Settings window centering needs adjustment")
# Keep window open for 3 seconds to visually verify
root.after(3000, root.quit)
root.mainloop()
except Exception as e:
print(f"❌ Error testing settings window: {e}")
def test_exit_dialog_centering():
"""Test exit dialog centering"""
print("\nTesting exit dialog centering...")
# Create a simple test for the centering function
root = tk.Tk()
root.withdraw()
# Create a test dialog
dialog = tk.Toplevel(root)
dialog.title("Test Exit Dialog")
dialog.geometry("400x200")
dialog.configure(bg='#2d2d2d')
dialog.resizable(False, False)
# Test the centering logic
dialog.update_idletasks()
screen_width = dialog.winfo_screenwidth()
screen_height = dialog.winfo_screenheight()
dialog_width = 400
dialog_height = 200
# Calculate center position
center_x = int((screen_width - dialog_width) / 2)
center_y = int((screen_height - dialog_height) / 2)
# Ensure the dialog doesn't go off-screen
center_x = max(0, min(center_x, screen_width - dialog_width))
center_y = max(0, min(center_y, screen_height - dialog_height))
dialog.geometry(f"{dialog_width}x{dialog_height}+{center_x}+{center_y}")
dialog.lift()
# Add test content
tk.Label(dialog, text="🎬 Test Exit Dialog",
font=('Arial', 16, 'bold'),
fg='white', bg='#2d2d2d').pack(pady=20)
tk.Label(dialog, text="This dialog should be centered on screen",
font=('Arial', 12),
fg='white', bg='#2d2d2d').pack(pady=10)
# Get actual position
dialog.update_idletasks()
actual_x = dialog.winfo_x()
actual_y = dialog.winfo_y()
print(f"Screen size: {screen_width}x{screen_height}")
print(f"Dialog position: {actual_x}, {actual_y}")
print(f"Expected center: {center_x}, {center_y}")
# Check centering
margin = 50
is_centered_x = abs(actual_x - center_x) <= margin
is_centered_y = abs(actual_y - center_y) <= margin
if is_centered_x and is_centered_y:
print("✅ Exit dialog is properly centered!")
else:
print("❌ Exit dialog centering needs adjustment")
# Close dialog after 3 seconds
root.after(3000, root.quit)
root.mainloop()
if __name__ == "__main__":
print("🧪 Testing Window Centering Functionality")
print("=" * 50)
test_settings_centering()
test_exit_dialog_centering()
print("\n✅ Centering tests completed!")
print("\nThe windows should appear centered on your screen regardless of resolution.")
print("This works for any screen size: 1024x768, 1920x1080, 4K, etc.")

View File

@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify image display functionality
"""
import tkinter as tk
from PIL import Image, ImageTk
import os
def test_image_display():
# Create a simple tkinter window
root = tk.Tk()
root.title("Image Display Test")
root.geometry("800x600")
root.configure(bg='black')
# Create image label
image_label = tk.Label(root, bg='black')
image_label.pack(fill=tk.BOTH, expand=True)
# Test image path
test_image = "/home/pi/Desktop/signage-player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg"
try:
if os.path.exists(test_image):
print(f"Loading image: {test_image}")
# Load and display image
img = Image.open(test_image)
img.thumbnail((800, 600), Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
image_label.config(image=photo)
image_label.image = photo # Keep reference
print(f"Image loaded successfully: {img.size}")
# Close after 3 seconds
root.after(3000, root.quit)
else:
print(f"Image file not found: {test_image}")
image_label.config(text="Image file not found", fg='white')
root.after(2000, root.quit)
except Exception as e:
print(f"Error loading image: {e}")
image_label.config(text=f"Error: {e}", fg='red')
root.after(2000, root.quit)
root.mainloop()
print("Image display test completed")
if __name__ == "__main__":
test_image_display()

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import sys
print("Testing imports...")
try:
import tkinter as tk
print("✓ tkinter imported successfully")
except Exception as e:
print(f"✗ tkinter import failed: {e}")
try:
from PIL import Image, ImageTk
print("✓ PIL and ImageTk imported successfully")
except Exception as e:
print(f"✗ PIL import failed: {e}")
try:
from virtual_keyboard import VirtualKeyboard
print("✓ Virtual keyboard imported successfully")
except Exception as e:
print(f"✗ Virtual keyboard import failed: {e}")
try:
from python_functions import load_local_playlist
print("✓ Python functions imported successfully")
# Test loading playlist
playlist_data = load_local_playlist()
playlist = playlist_data.get('playlist', [])
print(f"✓ Local playlist loaded: {len(playlist)} items")
for i, item in enumerate(playlist):
print(f" {i+1}. {item.get('file_name', 'Unknown')}")
except Exception as e:
print(f"✗ Python functions import/execution failed: {e}")
print("Import test completed")

View File

@@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""
Test script for full-screen image scaling functionality
"""
import tkinter as tk
from PIL import Image, ImageTk
import os
def test_scaling_modes():
"""Test different scaling modes for images"""
def scale_image_to_screen(img, screen_width, screen_height, mode='fit'):
"""Test scaling function"""
img_width, img_height = img.size
if mode == 'stretch':
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
elif mode == 'fill':
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_height = screen_height
new_width = int(screen_height * img_ratio)
x_offset = (screen_width - new_width) // 2
y_offset = 0
else:
new_width = screen_width
new_height = int(screen_width / img_ratio)
x_offset = 0
y_offset = (screen_height - new_height) // 2
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
if new_width > screen_width:
crop_x = (new_width - screen_width) // 2
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
x_offset = 0
if new_height > screen_height:
crop_y = (new_height - screen_height) // 2
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
y_offset = 0
final_img.paste(img_resized, (x_offset, y_offset))
return final_img, (x_offset, y_offset)
else: # fit mode
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_width = screen_width
new_height = int(screen_width / img_ratio)
else:
new_height = screen_height
new_width = int(screen_height * img_ratio)
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
x_offset = (screen_width - new_width) // 2
y_offset = (screen_height - new_height) // 2
final_img.paste(img_resized, (x_offset, y_offset))
return final_img, (x_offset, y_offset)
# Test image path
test_image = "/home/pi/Desktop/signage-player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg"
if not os.path.exists(test_image):
print(f"Test image not found: {test_image}")
return
try:
# Load test image
img = Image.open(test_image)
original_size = img.size
screen_width, screen_height = 800, 600
print(f"Testing scaling modes for image: {original_size}")
print(f"Target screen size: {screen_width}x{screen_height}")
# Test each scaling mode
modes = ['fit', 'fill', 'stretch']
for mode in modes:
final_img, offset = scale_image_to_screen(img, screen_width, screen_height, mode)
print(f"{mode.upper()} mode: Final size: {final_img.size}, Offset: {offset}")
print("✅ All scaling modes tested successfully!")
except Exception as e:
print(f"❌ Error testing scaling: {e}")
if __name__ == "__main__":
test_scaling_modes()

View File

@@ -1,230 +0,0 @@
#!/usr/bin/env python3
"""
Touch Display Test - Test the touch-optimized interface with virtual keyboard
"""
import tkinter as tk
import sys
import os
sys.path.append('tkinter_app/src')
def test_touch_interface():
"""Test the touch-optimized settings interface"""
try:
from tkinter_simple_player import SettingsWindow
# Create main window
root = tk.Tk()
root.title("🎮 Touch Display Test")
root.geometry("1024x768")
root.configure(bg='#2c3e50')
# Create welcome screen
welcome_frame = tk.Frame(root, bg='#2c3e50', padx=40, pady=40)
welcome_frame.pack(fill=tk.BOTH, expand=True)
# Title
title_label = tk.Label(welcome_frame,
text="🎬 Touch Display Digital Signage",
font=('Segoe UI', 24, 'bold'),
fg='white', bg='#2c3e50')
title_label.pack(pady=30)
# Description
desc_text = (
"Touch-Optimized Features:\n\n"
"📱 Virtual On-Screen Keyboard\n"
"🎯 Larger Touch-Friendly Buttons\n"
"⌨️ Auto-Show Keyboard on Input Focus\n"
"👆 Enhanced Touch Feedback\n"
"🎨 Dark Theme Optimized for Displays\n\n"
"Click the button below to test the settings interface:"
)
desc_label = tk.Label(welcome_frame, text=desc_text,
font=('Segoe UI', 14),
fg='#ecf0f1', bg='#2c3e50',
justify=tk.CENTER)
desc_label.pack(pady=20)
# Create mock app for testing
class MockApp:
def __init__(self):
self.playlist = []
self.current_index = 0
def play_current_media(self):
print("Mock: play_current_media called")
mock_app = MockApp()
# Test button to open touch-optimized settings
def open_touch_settings():
try:
settings = SettingsWindow(root, mock_app)
print("✅ Touch-optimized settings window opened successfully!")
except Exception as e:
print(f"❌ Error opening settings: {e}")
import traceback
traceback.print_exc()
# Large touch-friendly button
settings_btn = tk.Button(welcome_frame,
text="🔧 Open Touch Settings",
command=open_touch_settings,
bg='#3498db', fg='white',
font=('Segoe UI', 16, 'bold'),
relief=tk.FLAT, padx=40, pady=20,
cursor='hand2')
settings_btn.pack(pady=30)
# Instructions
instructions = (
"Touch Instructions:\n"
"• Tap input fields to show virtual keyboard\n"
"• Use large buttons for easy touch interaction\n"
"• Virtual keyboard stays on top for easy access\n"
"• Click outside input fields to hide keyboard"
)
instr_label = tk.Label(welcome_frame, text=instructions,
font=('Segoe UI', 11),
fg='#bdc3c7', bg='#2c3e50',
justify=tk.LEFT)
instr_label.pack(pady=20)
# Exit button
exit_btn = tk.Button(welcome_frame,
text="❌ Exit Test",
command=root.quit,
bg='#e74c3c', fg='white',
font=('Segoe UI', 12, 'bold'),
relief=tk.FLAT, padx=30, pady=15,
cursor='hand2')
exit_btn.pack(pady=20)
# Add touch feedback to buttons
def add_touch_feedback(button):
def on_press(e):
button.configure(relief=tk.SUNKEN)
def on_release(e):
button.configure(relief=tk.FLAT)
def on_enter(e):
button.configure(relief=tk.RAISED)
def on_leave(e):
button.configure(relief=tk.FLAT)
button.bind("<Button-1>", on_press)
button.bind("<ButtonRelease-1>", on_release)
button.bind("<Enter>", on_enter)
button.bind("<Leave>", on_leave)
add_touch_feedback(settings_btn)
add_touch_feedback(exit_btn)
print("🎮 Touch Display Test Started")
print("=" * 50)
print("Features being tested:")
print("- Virtual keyboard integration")
print("- Touch-optimized input fields")
print("- Large, finger-friendly buttons")
print("- Enhanced visual feedback")
print("- Dark theme for displays")
print("\nClick 'Open Touch Settings' to test the interface!")
root.mainloop()
except Exception as e:
print(f"❌ Error in touch interface test: {e}")
import traceback
traceback.print_exc()
def test_virtual_keyboard_standalone():
"""Test just the virtual keyboard component"""
try:
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
root = tk.Tk()
root.title("🎹 Virtual Keyboard Test")
root.geometry("800x500")
root.configure(bg='#2f3136')
# Create virtual keyboard
vk = VirtualKeyboard(root, dark_theme=True)
# Test interface
test_frame = tk.Frame(root, bg='#2f3136', padx=30, pady=30)
test_frame.pack(fill=tk.BOTH, expand=True)
tk.Label(test_frame, text="🎹 Virtual Keyboard Test",
font=('Segoe UI', 20, 'bold'),
bg='#2f3136', fg='white').pack(pady=20)
tk.Label(test_frame, text="Click on the input fields below to test the virtual keyboard:",
font=('Segoe UI', 12),
bg='#2f3136', fg='#b9bbbe').pack(pady=10)
# Test input fields
tk.Label(test_frame, text="Server IP:", bg='#2f3136', fg='white',
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(20, 5))
entry1 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
fg='white', insertbackground='white')
entry1.pack(pady=5, fill=tk.X)
tk.Label(test_frame, text="Device Name:", bg='#2f3136', fg='white',
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(15, 5))
entry2 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
fg='white', insertbackground='white')
entry2.pack(pady=5, fill=tk.X)
tk.Label(test_frame, text="Password:", bg='#2f3136', fg='white',
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(15, 5))
entry3 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
fg='white', insertbackground='white', show='*')
entry3.pack(pady=5, fill=tk.X)
# Control buttons
btn_frame = tk.Frame(test_frame, bg='#2f3136')
btn_frame.pack(pady=30)
TouchOptimizedButton(btn_frame, text="🎹 Show Keyboard",
command=lambda: vk.show_keyboard(entry1),
bg='#7289da', fg='white').pack(side=tk.LEFT, padx=10)
TouchOptimizedButton(btn_frame, text="❌ Hide Keyboard",
command=vk.hide_keyboard,
bg='#ed4245', fg='white').pack(side=tk.LEFT, padx=10)
TouchOptimizedButton(btn_frame, text="🔄 Clear All",
command=lambda: [e.delete(0, tk.END) for e in [entry1, entry2, entry3]],
bg='#faa61a', fg='white').pack(side=tk.LEFT, padx=10)
print("🎹 Virtual Keyboard Test Started")
print("- Click input fields to auto-show keyboard")
print("- Type using virtual or physical keyboard")
print("- Test touch-friendly interface")
root.mainloop()
except Exception as e:
print(f"❌ Error in virtual keyboard test: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Touch Display Tests")
parser.add_argument("--keyboard-only", action="store_true",
help="Test only the virtual keyboard component")
args = parser.parse_args()
print("🎮 Touch Display Digital Signage Tests")
print("=" * 50)
if args.keyboard_only:
test_virtual_keyboard_standalone()
else:
test_touch_interface()
print("\n✅ Touch display tests completed!")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 KiB

View File

@@ -1,15 +0,0 @@
{
"playlist": [
{
"file_name": "demo1.jpg",
"url": "Resurse/demo1.jpg",
"duration": 20
},
{
"file_name": "demo2.jpg",
"url": "Resurse/demo2.jpg",
"duration": 20
}
],
"version": 1
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,15 +0,0 @@
{
"playlist": [
{
"file_name": "Cindrel_1.jpg",
"url": "static/resurse/Cindrel_1.jpg",
"duration": 10
},
{
"file_name": "trans_cindrel_4.jpg",
"url": "static/resurse/trans_cindrel_4.jpg",
"duration": 10
}
],
"version": 29
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show More