Compare commits
17 Commits
2a564f5e84
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 21722c5c85 | |||
| ad4d71e0b6 | |||
| d7f7df49e7 | |||
| 9c42b38c4b | |||
| 0aa1bb7069 | |||
| 35db99eb3d | |||
| b6e6190d6c | |||
| 26a9db889f | |||
| 26fc946a65 | |||
| a91b07ede4 | |||
| 185f3099ad | |||
| 5063b47a56 | |||
| e2eecb9cf9 | |||
| bd4f101fcc | |||
| cb861d0ffa | |||
| 02d13b2eaa | |||
| d2a996feb9 |
59
ARCHITECTURE_INSTALL_GUIDE.md
Normal file
59
ARCHITECTURE_INSTALL_GUIDE.md
Normal 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
|
||||
@@ -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
44
OFFLINE_INSTALL_README.md
Normal 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
150
README.md
@@ -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
29
build_pillow_local.sh
Executable 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
89
download_32bit_libs.sh
Executable 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
107
install_32bit.sh
Executable 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"
|
||||
@@ -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
68
install_offline.sh
Executable 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"
|
||||
@@ -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"
|
||||
BIN
req_libraries/MouseInfo-0.1.3-py3-none-any.whl
Normal file
BIN
req_libraries/MouseInfo-0.1.3-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/PyAutoGUI-0.9.54-py3-none-any.whl
Normal file
BIN
req_libraries/PyAutoGUI-0.9.54-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/PyGetWindow-0.0.9-py3-none-any.whl
Normal file
BIN
req_libraries/PyGetWindow-0.0.9-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/PyRect-0.2.0-py2.py3-none-any.whl
Normal file
BIN
req_libraries/PyRect-0.2.0-py2.py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/PyScreeze-1.0.1-py3-none-any.whl
Normal file
BIN
req_libraries/PyScreeze-1.0.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl
Normal file
BIN
req_libraries/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl
Normal file
Binary file not shown.
BIN
req_libraries/certifi-2025.8.3-py3-none-any.whl
Normal file
BIN
req_libraries/certifi-2025.8.3-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
req_libraries/idna-3.10-py3-none-any.whl
Normal file
BIN
req_libraries/idna-3.10-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
req_libraries/pymsgbox-2.0.1-py3-none-any.whl
Normal file
BIN
req_libraries/pymsgbox-2.0.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/pyperclip-1.9.0-py3-none-any.whl
Normal file
BIN
req_libraries/pyperclip-1.9.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/python3_xlib-0.15-py3-none-any.whl
Normal file
BIN
req_libraries/python3_xlib-0.15-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/python_vlc-3.0.21203-py3-none-any.whl
Normal file
BIN
req_libraries/python_vlc-3.0.21203-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/pytweening-1.2.0-py3-none-any.whl
Normal file
BIN
req_libraries/pytweening-1.2.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/requests-2.32.5-py3-none-any.whl
Normal file
BIN
req_libraries/requests-2.32.5-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries/urllib3-2.5.0-py3-none-any.whl
Normal file
BIN
req_libraries/urllib3-2.5.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/MouseInfo-0.1.3-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/MouseInfo-0.1.3-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/PyAutoGUI-0.9.54-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyAutoGUI-0.9.54-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/PyGetWindow-0.0.9-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyGetWindow-0.0.9-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/PyRect-0.2.0-py2.py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyRect-0.2.0-py2.py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/PyScreeze-1.0.1-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyScreeze-1.0.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/bcrypt-4.3.0-cp311-cp311-linux_armv7l.whl
Normal file
BIN
req_libraries_32bit/bcrypt-4.3.0-cp311-cp311-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/certifi-2025.8.3-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/certifi-2025.8.3-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/charset_normalizer-3.4.3-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/charset_normalizer-3.4.3-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/idna-3.10-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/idna-3.10-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/pillow-11.3.0-cp311-cp311-linux_armv7l.whl
Normal file
BIN
req_libraries_32bit/pillow-11.3.0-cp311-cp311-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/pillow-11.3.0.tar.gz
Normal file
BIN
req_libraries_32bit/pillow-11.3.0.tar.gz
Normal file
Binary file not shown.
BIN
req_libraries_32bit/pymsgbox-2.0.1-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/pymsgbox-2.0.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/pyperclip-1.9.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/pyperclip-1.9.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/python3_xlib-0.15-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/python3_xlib-0.15-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/python_vlc-3.0.21203-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/python_vlc-3.0.21203-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/pytweening-1.2.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/pytweening-1.2.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/requests-2.32.5-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/requests-2.32.5-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/setuptools-80.9.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/setuptools-80.9.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/urllib3-2.5.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/urllib3-2.5.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
req_libraries_32bit/wheel-0.45.1-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/wheel-0.45.1-py3-none-any.whl
Normal file
Binary file not shown.
@@ -1,5 +1,7 @@
|
||||
# Core requirements for tkinter_player signage app
|
||||
python-vlc
|
||||
Pillow
|
||||
pyautogui
|
||||
requests
|
||||
bcrypt
|
||||
python-vlc
|
||||
pyautogui
|
||||
setuptools
|
||||
wheel
|
||||
35
run_app.sh
Executable file
35
run_app.sh
Executable 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
|
||||
@@ -1,14 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Debugging launch script for the tkinter player application
|
||||
|
||||
# Activate the virtual environment
|
||||
source venv/bin/activate
|
||||
# Set working directory to script location
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Change to the tkinter app src directory
|
||||
cd signage_player/src
|
||||
# Activate the virtual environment if it exists
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
echo "Virtual environment activated"
|
||||
else
|
||||
echo "Warning: No virtual environment found, using system Python"
|
||||
fi
|
||||
|
||||
# Set environment variables for better display on Raspberry Pi
|
||||
export DISPLAY=${DISPLAY:-:0.0}
|
||||
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp/runtime-pi}
|
||||
|
||||
# Change to the signage_player directory
|
||||
cd signage_player
|
||||
|
||||
# Run the main application with full error output
|
||||
python main.py
|
||||
echo "Starting tkinter player..."
|
||||
echo "Working directory: $(pwd)"
|
||||
echo "Python path: $(which python3)"
|
||||
echo "Display: $DISPLAY"
|
||||
|
||||
# Run with error logging
|
||||
python3 main.py 2>&1 | tee ../main_data/app_debug.log
|
||||
|
||||
# Capture exit code
|
||||
EXIT_CODE=$?
|
||||
|
||||
# Deactivate virtual environment when done
|
||||
if [ -d "../venv" ]; then
|
||||
deactivate
|
||||
fi
|
||||
|
||||
echo "Application exited with code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
Binary file not shown.
Binary file not shown.
@@ -12,14 +12,34 @@ class AppSettingsWindow(tk.Tk):
|
||||
self.geometry('440x600') # Increased height for better button visibility
|
||||
self.resizable(False, False)
|
||||
self.config(bg='#23272e')
|
||||
|
||||
# Ensure window appears on top and gets focus
|
||||
self.attributes('-topmost', True)
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
self.grab_set() # Make window modal
|
||||
|
||||
# Center the window on screen
|
||||
self.center_window()
|
||||
|
||||
self.fields = {}
|
||||
self.load_config()
|
||||
self.style = ttk.Style(self)
|
||||
self.set_styles()
|
||||
self.create_widgets()
|
||||
|
||||
# Ensure focus after widgets are created
|
||||
self.after(100, self.focus_force)
|
||||
|
||||
def center_window(self):
|
||||
"""Center the settings window on the screen"""
|
||||
self.update_idletasks()
|
||||
width = self.winfo_width()
|
||||
height = self.winfo_height()
|
||||
x = (self.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (self.winfo_screenheight() // 2) - (height // 2)
|
||||
self.geometry(f'{width}x{height}+{x}+{y}')
|
||||
|
||||
def set_styles(self):
|
||||
self.style.theme_use('clam')
|
||||
self.style.configure('TLabel', background='#23272e', foreground='#e0e0e0', font=('Segoe UI', 13, 'bold'))
|
||||
|
||||
@@ -3,8 +3,204 @@ import json
|
||||
import requests
|
||||
import bcrypt
|
||||
import re
|
||||
import datetime
|
||||
from logging_config import Logger
|
||||
|
||||
# Global variable to track server connectivity status
|
||||
SERVER_CONNECTION_STATUS = {
|
||||
'is_online': True,
|
||||
'last_successful_connection': None,
|
||||
'last_playlist_update': None,
|
||||
'error_message': None
|
||||
}
|
||||
|
||||
def get_server_status():
|
||||
"""Get current server connection status"""
|
||||
return SERVER_CONNECTION_STATUS.copy()
|
||||
|
||||
def get_last_playlist_update_time():
|
||||
"""Get the timestamp of the last playlist update from filesystem"""
|
||||
try:
|
||||
playlist_dir = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||
if os.path.exists(playlist_dir):
|
||||
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
if playlist_files:
|
||||
# Get the most recent playlist file
|
||||
latest_file = max([os.path.join(playlist_dir, f) for f in playlist_files], key=os.path.getmtime)
|
||||
mod_time = os.path.getmtime(latest_file)
|
||||
return datetime.datetime.fromtimestamp(mod_time)
|
||||
return None
|
||||
except Exception as e:
|
||||
Logger.error(f"Error getting last playlist update time: {e}")
|
||||
return None
|
||||
|
||||
def set_server_offline(error_message=None):
|
||||
"""Mark server as offline with optional error message"""
|
||||
global SERVER_CONNECTION_STATUS
|
||||
SERVER_CONNECTION_STATUS['is_online'] = False
|
||||
SERVER_CONNECTION_STATUS['error_message'] = error_message
|
||||
Logger.warning(f"Server marked as offline: {error_message}")
|
||||
|
||||
def set_server_online():
|
||||
"""Mark server as online and update connection time"""
|
||||
global SERVER_CONNECTION_STATUS
|
||||
SERVER_CONNECTION_STATUS['is_online'] = True
|
||||
SERVER_CONNECTION_STATUS['last_successful_connection'] = datetime.datetime.now()
|
||||
SERVER_CONNECTION_STATUS['error_message'] = None
|
||||
Logger.info("Server connection restored")
|
||||
|
||||
def send_player_feedback(config, message, status="active", playlist_version=None, error_details=None):
|
||||
"""
|
||||
Send feedback to the server about player status.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
message (str): Main feedback message
|
||||
status (str): Player status - "active", "playing", "error", "restarting"
|
||||
playlist_version (int, optional): Current playlist version being played
|
||||
error_details (str, optional): Error details if status is "error"
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
server = config.get("server_ip", "")
|
||||
host = config.get("screen_name", "")
|
||||
quick = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
|
||||
# Construct server URL
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server):
|
||||
feedback_url = f'http://{server}:{port}/api/player-feedback'
|
||||
else:
|
||||
feedback_url = f'http://{server}/api/player-feedback'
|
||||
|
||||
# Prepare feedback data
|
||||
feedback_data = {
|
||||
'player_name': host,
|
||||
'quickconnect_code': quick,
|
||||
'message': message,
|
||||
'status': status,
|
||||
'timestamp': datetime.datetime.now().isoformat(),
|
||||
'playlist_version': playlist_version,
|
||||
'error_details': error_details
|
||||
}
|
||||
|
||||
Logger.info(f"Sending feedback to {feedback_url}: {feedback_data}")
|
||||
|
||||
# Send POST request
|
||||
response = requests.post(feedback_url, json=feedback_data, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
Logger.info(f"Feedback sent successfully: {message}")
|
||||
# Mark server as online on successful feedback
|
||||
set_server_online()
|
||||
return True
|
||||
else:
|
||||
Logger.warning(f"Feedback failed with status {response.status_code}: {response.text}")
|
||||
set_server_offline(f"Feedback failed with status {response.status_code}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Failed to send feedback: {e}")
|
||||
set_server_offline(f"Network error during feedback: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
Logger.error(f"Unexpected error sending feedback: {e}")
|
||||
set_server_offline(f"Unexpected error during feedback: {e}")
|
||||
return False
|
||||
|
||||
def send_playlist_check_feedback(config, playlist_version=None):
|
||||
"""
|
||||
Send feedback when server is interrogated for playlist updates.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, server interrogation, checking for updates : {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="active",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def send_playlist_restart_feedback(config, playlist_version=None):
|
||||
"""
|
||||
Send feedback when playlist loop ends and restarts.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, playlist working in loop, cycle completed : {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="restarting",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def send_player_error_feedback(config, error_message, playlist_version=None):
|
||||
"""
|
||||
Send feedback when an error occurs in the player.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
error_message (str): Description of the error
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
message = f"player {player_name}, error occurred"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="error",
|
||||
playlist_version=playlist_version,
|
||||
error_details=error_message
|
||||
)
|
||||
|
||||
def send_playing_status_feedback(config, playlist_version=None, current_media=None):
|
||||
"""
|
||||
Send feedback about playlist starting (first media).
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
current_media (str, optional): First media file in playlist
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, playlist started : {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="playing",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def is_playlist_up_to_date(local_playlist_path, config):
|
||||
"""
|
||||
Compare the version of the local playlist with the server playlist.
|
||||
@@ -39,25 +235,44 @@ def fetch_server_playlist(config):
|
||||
'quickconnect_code': quick
|
||||
}
|
||||
Logger.info(f"Fetching playlist from URL: {server_url} with params: {params}")
|
||||
response = requests.get(server_url, params=params)
|
||||
response = requests.get(server_url, params=params, timeout=15)
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
Logger.info(f"Server response: {response_data}")
|
||||
playlist = response_data.get('playlist', [])
|
||||
version = response_data.get('playlist_version', None)
|
||||
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
|
||||
|
||||
if version is not None and hashed_quickconnect is not None:
|
||||
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
|
||||
Logger.info("Fetched updated playlist from server.")
|
||||
# Mark server as online on successful connection
|
||||
set_server_online()
|
||||
return {'playlist': playlist, 'version': version}
|
||||
else:
|
||||
Logger.error("Quickconnect code validation failed.")
|
||||
set_server_offline("Authentication failed - invalid quickconnect code")
|
||||
else:
|
||||
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
set_server_offline("Invalid server response - missing playlist data")
|
||||
else:
|
||||
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
set_server_offline(f"Server returned error code: {response.status_code}")
|
||||
|
||||
except requests.exceptions.ConnectTimeout as e:
|
||||
Logger.error(f"Connection timeout while fetching playlist: {e}")
|
||||
set_server_offline("Connection timeout - server unreachable")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
Logger.error(f"Connection error while fetching playlist: {e}")
|
||||
set_server_offline("Connection failed - server unreachable")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Failed to fetch playlist: {e}")
|
||||
Logger.error(f"Request error while fetching playlist: {e}")
|
||||
set_server_offline(f"Network error: {str(e)}")
|
||||
except Exception as e:
|
||||
Logger.error(f"Unexpected error while fetching playlist: {e}")
|
||||
set_server_offline(f"Unexpected error: {str(e)}")
|
||||
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
def save_playlist_with_version(playlist_data, playlist_dir):
|
||||
@@ -141,8 +356,11 @@ def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_d
|
||||
"""
|
||||
Fetch the server playlist once, compare versions, and update if needed.
|
||||
Returns True if updated, False if already up to date.
|
||||
Also sends feedback to server about playlist check.
|
||||
"""
|
||||
import json
|
||||
global SERVER_CONNECTION_STATUS
|
||||
|
||||
server_data = fetch_server_playlist(config)
|
||||
server_version = server_data.get('version', 0)
|
||||
if not os.path.exists(local_playlist_path):
|
||||
@@ -151,20 +369,30 @@ def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_d
|
||||
with open(local_playlist_path, 'r') as f:
|
||||
local_data = json.load(f)
|
||||
local_version = local_data.get('version', 0)
|
||||
|
||||
Logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
|
||||
if local_version != server_version:
|
||||
|
||||
# Only send feedback if server is online
|
||||
if SERVER_CONNECTION_STATUS['is_online']:
|
||||
send_playlist_check_feedback(config, server_version if server_version > 0 else local_version)
|
||||
|
||||
if local_version != server_version and server_version > 0:
|
||||
if server_data and server_data.get('playlist'):
|
||||
updated_playlist = download_media_files(server_data['playlist'], media_dir)
|
||||
server_data['playlist'] = updated_playlist
|
||||
save_playlist_with_version(server_data, playlist_dir)
|
||||
# Delete old playlists and unreferenced media
|
||||
delete_old_playlists_and_media(server_version, playlist_dir, media_dir)
|
||||
|
||||
# Update last playlist update time
|
||||
SERVER_CONNECTION_STATUS['last_playlist_update'] = datetime.datetime.now()
|
||||
|
||||
return True
|
||||
else:
|
||||
Logger.warning("No playlist data fetched from server or playlist is empty.")
|
||||
return False
|
||||
else:
|
||||
Logger.info("Local playlist is already up to date.")
|
||||
Logger.info("Local playlist is already up to date or server is offline.")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -54,8 +54,11 @@ def main():
|
||||
def reload_playlist_if_updated():
|
||||
new_playlist = load_latest_playlist()
|
||||
if new_playlist != player.playlist:
|
||||
print("[MAIN] Playlist updated, reloading...")
|
||||
player.playlist = new_playlist
|
||||
player.current_index = 0
|
||||
# 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()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "tv-terasa",
|
||||
"quickconnect_key": "8887779",
|
||||
"server_ip": "10.232.7.231",
|
||||
"server_ip": "192.168.1.22",
|
||||
"port": "80",
|
||||
"screen_w": "1920",
|
||||
"screen_h": "1080",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
423
signage_player/old_code/playerjoi 4 sept.py
Normal file
423
signage_player/old_code/playerjoi 4 sept.py
Normal file
@@ -0,0 +1,423 @@
|
||||
import os
|
||||
import json
|
||||
import tkinter as tk
|
||||
import vlc
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
|
||||
|
||||
class SimpleTkPlayer:
|
||||
def __init__(self, root, playlist):
|
||||
self.root = root
|
||||
self.playlist = playlist
|
||||
self.current_index = 0
|
||||
self.paused = False
|
||||
self.pause_timer = None
|
||||
self.label = tk.Label(root, bg='black')
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
self.create_controls()
|
||||
self.hide_controls()
|
||||
self.root.bind('<Motion>', self.on_activity)
|
||||
self.root.bind('<Button-1>', self.on_activity)
|
||||
self.root.after(100, self.ensure_fullscreen)
|
||||
self.root.after(200, self.hide_mouse)
|
||||
self.root.after(300, self.move_mouse_to_corner)
|
||||
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
|
||||
|
||||
def ensure_fullscreen(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def create_controls(self):
|
||||
# Create a transparent, borderless top-level window for controls
|
||||
self.controls_win = tk.Toplevel(self.root)
|
||||
self.controls_win.overrideredirect(True)
|
||||
self.controls_win.attributes('-topmost', True)
|
||||
self.controls_win.attributes('-alpha', 0.92)
|
||||
self.controls_win.configure(bg='')
|
||||
# Place the window at the bottom right
|
||||
def place_controls():
|
||||
self.controls_win.update_idletasks()
|
||||
w = self.controls_win.winfo_reqwidth()
|
||||
h = self.controls_win.winfo_reqheight()
|
||||
sw = self.root.winfo_screenwidth()
|
||||
sh = self.root.winfo_screenheight()
|
||||
x = sw - w - 30
|
||||
y = sh - h - 30
|
||||
self.controls_win.geometry(f'+{x}+{y}')
|
||||
self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge')
|
||||
self.controls_frame.pack()
|
||||
btn_style = {
|
||||
'bg': '#333',
|
||||
'fg': 'white',
|
||||
'activebackground': '#555',
|
||||
'activeforeground': '#00e6e6',
|
||||
'font': ('Arial', 16, 'bold'),
|
||||
'bd': 0,
|
||||
'highlightthickness': 0,
|
||||
'relief': 'flat',
|
||||
'cursor': 'hand2',
|
||||
'padx': 10,
|
||||
'pady': 6
|
||||
}
|
||||
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
|
||||
self.prev_btn.grid(row=0, column=0, padx=4)
|
||||
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
|
||||
self.pause_btn.grid(row=0, column=1, padx=4)
|
||||
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
|
||||
self.next_btn.grid(row=0, column=2, padx=4)
|
||||
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
|
||||
self.settings_btn.grid(row=0, column=3, padx=4)
|
||||
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
|
||||
self.exit_btn.grid(row=0, column=4, padx=4)
|
||||
self.exit_btn.config(fg='#ff4d4d')
|
||||
self.controls_win.withdraw()
|
||||
self.controls_win.after(200, place_controls)
|
||||
self.root.bind('<Configure>', lambda e: self.controls_win.after(200, place_controls))
|
||||
|
||||
def hide_mouse(self):
|
||||
self.root.config(cursor='none')
|
||||
if hasattr(self, 'controls_win'):
|
||||
self.controls_win.config(cursor='none')
|
||||
|
||||
def show_mouse(self):
|
||||
self.root.config(cursor='arrow')
|
||||
if hasattr(self, 'controls_win'):
|
||||
self.controls_win.config(cursor='arrow')
|
||||
|
||||
def move_mouse_to_corner(self):
|
||||
try:
|
||||
import pyautogui
|
||||
sw = self.root.winfo_screenwidth()
|
||||
sh = self.root.winfo_screenheight()
|
||||
pyautogui.moveTo(sw-2, sh-2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def show_controls(self):
|
||||
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
|
||||
self.create_controls()
|
||||
self.controls_win.deiconify()
|
||||
self.controls_win.lift()
|
||||
self.show_mouse()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
def hide_controls(self):
|
||||
self.controls_win.withdraw()
|
||||
self.hide_mouse()
|
||||
|
||||
def schedule_hide_controls(self):
|
||||
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||
self.root.after_cancel(self.hide_controls_timer)
|
||||
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
|
||||
|
||||
def on_activity(self, event=None):
|
||||
self.show_controls()
|
||||
|
||||
def prev_media(self):
|
||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def next_media(self):
|
||||
# If at the last media, update playlist before looping
|
||||
if self.current_index == len(self.playlist) - 1:
|
||||
self.update_playlist_from_server()
|
||||
self.current_index = 0
|
||||
self.show_current_media()
|
||||
else:
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def update_playlist_from_server(self):
|
||||
# Dummy implementation: replace with your actual update logic
|
||||
# For example, call a function to fetch and reload the playlist
|
||||
print("[INFO] Updating playlist from server...")
|
||||
# You can import and call your real update function here
|
||||
# Example: self.playlist = get_latest_playlist()
|
||||
|
||||
def toggle_pause(self):
|
||||
if not self.paused:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
self.pause_timer = self.root.after(30000, self.resume_play)
|
||||
else:
|
||||
self.resume_play()
|
||||
|
||||
def resume_play(self):
|
||||
self.paused = False
|
||||
self.pause_btn.config(text='⏸ Pause')
|
||||
if self.pause_timer:
|
||||
self.root.after_cancel(self.pause_timer)
|
||||
self.pause_timer = None
|
||||
|
||||
def play_intro_video(self):
|
||||
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
|
||||
if os.path.exists(intro_path):
|
||||
self.show_video(intro_path, on_end=self.after_intro)
|
||||
else:
|
||||
self.after_intro()
|
||||
|
||||
def after_intro(self):
|
||||
self.show_current_media()
|
||||
self.root.after(100, self.next_media_loop)
|
||||
|
||||
def show_video(self, file_path, on_end=None, duration=None):
|
||||
try:
|
||||
print(f"[PLAYER] Attempting to play video: {file_path}")
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
if not hasattr(self, 'video_canvas'):
|
||||
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.label.pack_forget()
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
self.vlc_instance = vlc.Instance('--vout=x11')
|
||||
self.vlc_player = self.vlc_instance.media_player_new()
|
||||
self.vlc_player.set_mrl(file_path)
|
||||
self.vlc_player.set_fullscreen(True)
|
||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||
self.vlc_player.play()
|
||||
# Watchdog timer: fallback if video doesn't end
|
||||
def watchdog():
|
||||
print(f"[WATCHDOG] Video watchdog triggered for {file_path}")
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
max_duration = duration if duration is not None else 60 # fallback max 60s
|
||||
self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog)
|
||||
def finish_video():
|
||||
if hasattr(self, 'video_watchdog'):
|
||||
self.root.after_cancel(self.video_watchdog)
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
if duration is not None:
|
||||
self.root.after(int(duration * 1000), finish_video)
|
||||
else:
|
||||
def check_end():
|
||||
try:
|
||||
if self.vlc_player.get_state() == vlc.State.Ended:
|
||||
finish_video()
|
||||
elif self.vlc_player.get_state() == vlc.State.Error:
|
||||
print(f"[VLC] Error state detected for {file_path}")
|
||||
finish_video()
|
||||
else:
|
||||
self.root.after(200, check_end)
|
||||
except Exception as e:
|
||||
print(f"[VLC] Exception in check_end: {e}")
|
||||
finish_video()
|
||||
check_end()
|
||||
except Exception as e:
|
||||
print(f"[VLC] Error playing video {file_path}: {e}")
|
||||
if on_end:
|
||||
on_end()
|
||||
|
||||
def show_current_media(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
if not self.playlist:
|
||||
print("[PLAYER] Playlist is empty. No media to show.")
|
||||
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
||||
# Try to reload playlist after 10 seconds
|
||||
self.root.after(10000, self.reload_playlist_and_continue)
|
||||
return
|
||||
media = self.playlist[self.current_index]
|
||||
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
||||
ext = file_path.lower()
|
||||
duration = media.get('duration', None)
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"[PLAYER] File missing: {file_path}. Skipping to next.")
|
||||
self.next_media()
|
||||
return
|
||||
if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||
self.show_video(file_path, on_end=self.next_media, duration=duration)
|
||||
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media)
|
||||
else:
|
||||
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
|
||||
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
||||
self.root.after(2000, self.next_media)
|
||||
|
||||
def reload_playlist_and_continue(self):
|
||||
print("[PLAYER] Attempting to reload playlist...")
|
||||
new_playlist = load_latest_playlist()
|
||||
if new_playlist:
|
||||
self.playlist = new_playlist
|
||||
self.current_index = 0
|
||||
print("[PLAYER] Playlist reloaded. Continuing playback.")
|
||||
self.show_current_media()
|
||||
else:
|
||||
print("[PLAYER] Still no playlist. Will retry.")
|
||||
self.root.after(10000, self.reload_playlist_and_continue)
|
||||
|
||||
def show_image_via_vlc(self, file_path, duration, on_end=None):
|
||||
try:
|
||||
print(f"[PLAYER] Attempting to show image: {file_path}")
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
if not hasattr(self, 'video_canvas'):
|
||||
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.label.pack_forget()
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
self.vlc_instance = vlc.Instance('--vout=x11')
|
||||
self.vlc_player = self.vlc_instance.media_player_new()
|
||||
self.vlc_player.set_mrl(file_path)
|
||||
self.vlc_player.set_fullscreen(True)
|
||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||
self.vlc_player.play()
|
||||
# Watchdog timer: fallback if image doesn't advance
|
||||
def watchdog():
|
||||
print(f"[WATCHDOG] Image watchdog triggered for {file_path}")
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
self.image_watchdog = self.root.after(int(duration * 1200), watchdog)
|
||||
def finish_image():
|
||||
if hasattr(self, 'image_watchdog'):
|
||||
self.root.after_cancel(self.image_watchdog)
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
self.root.after(int(duration * 1000), finish_image)
|
||||
except Exception as e:
|
||||
print(f"[VLC] Error showing image {file_path}: {e}")
|
||||
if on_end:
|
||||
on_end()
|
||||
|
||||
def next_media(self):
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def next_media_loop(self):
|
||||
if not self.playlist or self.paused:
|
||||
self.root.after(1000, self.next_media_loop)
|
||||
return
|
||||
self.show_current_media()
|
||||
|
||||
def exit_app(self):
|
||||
# Signal all threads and flags to stop
|
||||
if hasattr(self, 'stop_event') and self.stop_event:
|
||||
self.stop_event.set()
|
||||
if hasattr(self, 'app_running') and self.app_running:
|
||||
self.app_running[0] = False
|
||||
# Unbind all events to prevent callbacks after destroy
|
||||
try:
|
||||
self.root.unbind('<Motion>')
|
||||
self.root.unbind('<Button-1>')
|
||||
self.root.unbind('<Configure>')
|
||||
except Exception:
|
||||
pass
|
||||
# Attempt to destroy all Toplevel windows before root
|
||||
try:
|
||||
# Withdraw controls_win if it exists
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
if self.controls_win.winfo_exists():
|
||||
self.controls_win.withdraw()
|
||||
# Destroy controls_win if it exists (this will also destroy controls_frame)
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
if self.controls_win.winfo_exists():
|
||||
self.controls_win.destroy()
|
||||
self.controls_win = None
|
||||
# Fallback: destroy controls_frame if it somehow still exists
|
||||
if hasattr(self, 'controls_frame') and self.controls_frame:
|
||||
if self.controls_frame.winfo_exists():
|
||||
self.controls_frame.destroy()
|
||||
self.controls_frame = None
|
||||
# Fallback: destroy any remaining Toplevels in the app
|
||||
for widget in self.root.winfo_children():
|
||||
if isinstance(widget, tk.Toplevel):
|
||||
try:
|
||||
widget.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}")
|
||||
# Destroy any other Toplevels if needed (add here if you have more)
|
||||
try:
|
||||
if self.root.winfo_exists():
|
||||
self.root.destroy()
|
||||
except Exception as e:
|
||||
print(f"[EXIT] Error destroying root: {e}")
|
||||
|
||||
def open_settings(self):
|
||||
if self.paused is not True:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
# Explicitly pause VLC video if playing
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
try:
|
||||
self.vlc_player.pause()
|
||||
except Exception:
|
||||
pass
|
||||
# Destroy controls overlay so settings window is always interactive
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
self.controls_win.destroy()
|
||||
self.controls_win = None
|
||||
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
|
||||
# Open settings in a new process so it doesn't block the main player
|
||||
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
|
||||
# Give the window manager a moment to focus the new window
|
||||
self.root.after(300, lambda: self.root.focus_force())
|
||||
# Wait for the settings window to close, then resume
|
||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
||||
|
||||
def check_settings_closed(self, proc):
|
||||
if proc.poll() is not None:
|
||||
# Resume playback and unpause VLC if needed
|
||||
self.resume_play()
|
||||
# Restore and recreate controls overlay
|
||||
self.root.deiconify()
|
||||
self.create_controls()
|
||||
# Re-bind mouse and button events to new controls
|
||||
self.root.bind('<Motion>', self.on_activity)
|
||||
self.root.bind('<Button-1>', self.on_activity)
|
||||
self.show_controls()
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
try:
|
||||
# Only resume if it was paused by us
|
||||
if self.vlc_player.get_state() == vlc.State.Paused:
|
||||
self.vlc_player.play()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
||||
|
||||
def main_start(self):
|
||||
self.play_intro_video()
|
||||
|
||||
def load_latest_playlist():
|
||||
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
if not files:
|
||||
return []
|
||||
# Sort by version number descending
|
||||
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
|
||||
latest_file = files[0]
|
||||
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
|
||||
data = json.load(f)
|
||||
playlist = data.get('playlist', [])
|
||||
# Validate playlist: skip missing or unsupported files
|
||||
valid_exts = ('.mp4', '.avi', '.mov', '.mkv', '.jpg', '.jpeg', '.png', '.bmp', '.gif')
|
||||
valid_playlist = []
|
||||
for item in playlist:
|
||||
file_path = os.path.join(MEDIA_DATA_PATH, item.get('file_name', ''))
|
||||
if os.path.isfile(file_path) and file_path.lower().endswith(valid_exts):
|
||||
valid_playlist.append(item)
|
||||
else:
|
||||
print(f"[PLAYLIST] Skipping missing or unsupported file: {item.get('file_name')}")
|
||||
return valid_playlist
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 523 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "HARTING_Safety_day_informare_2_page_003.jpg",
|
||||
"url": "media/HARTING_Safety_day_informare_2_page_003.jpg",
|
||||
"file_name": "one-piece-season-2-5120x2880-23673.jpg",
|
||||
"url": "media/one-piece-season-2-5120x2880-23673.jpg",
|
||||
"duration": 30
|
||||
},
|
||||
{
|
||||
"file_name": "call-of-duty-black-3840x2160-23674.jpg",
|
||||
"url": "media/call-of-duty-black-3840x2160-23674.jpg",
|
||||
"duration": 30
|
||||
},
|
||||
{
|
||||
"file_name": "big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||
"url": "media/big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||
"duration": 30
|
||||
},
|
||||
{
|
||||
"file_name": "one-piece-season-2-5120x2880-23673.jpg",
|
||||
"url": "media/one-piece-season-2-5120x2880-23673.jpg",
|
||||
"duration": 30
|
||||
}
|
||||
],
|
||||
"version": 6
|
||||
"version": 8
|
||||
}
|
||||
81
troubleshoot_32bit.sh
Executable file
81
troubleshoot_32bit.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
# Troubleshooting script for 32-bit bcrypt issues
|
||||
|
||||
echo "🔍 32-BIT SYSTEM TROUBLESHOOTING"
|
||||
echo "================================"
|
||||
|
||||
# System info
|
||||
echo "1. System Information:"
|
||||
echo " Architecture: $(uname -m)"
|
||||
echo " OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
|
||||
echo " Python: $(python3 --version)"
|
||||
echo ""
|
||||
|
||||
# Check if we're in virtual environment
|
||||
echo "2. Environment Check:"
|
||||
if [ -n "$VIRTUAL_ENV" ]; then
|
||||
echo " ✅ In virtual environment: $VIRTUAL_ENV"
|
||||
echo " Python location: $(which python)"
|
||||
echo " Pip location: $(which pip)"
|
||||
else
|
||||
echo " ❌ Not in virtual environment"
|
||||
echo " System Python: $(which python3)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check bcrypt installation
|
||||
echo "3. Bcrypt Investigation:"
|
||||
if [ -n "$VIRTUAL_ENV" ]; then
|
||||
echo " Checking installed bcrypt..."
|
||||
pip list | grep bcrypt || echo " ❌ bcrypt not installed"
|
||||
|
||||
echo " Testing bcrypt import..."
|
||||
python3 -c "
|
||||
try:
|
||||
import bcrypt
|
||||
print(' ✅ bcrypt imports successfully')
|
||||
|
||||
# Check bcrypt module location
|
||||
print(f' Module location: {bcrypt.__file__}')
|
||||
|
||||
# Test basic functionality
|
||||
test_pw = b'test123'
|
||||
hashed = bcrypt.hashpw(test_pw, bcrypt.gensalt())
|
||||
if bcrypt.checkpw(test_pw, hashed):
|
||||
print(' ✅ bcrypt functionality works')
|
||||
else:
|
||||
print(' ❌ bcrypt functionality failed')
|
||||
|
||||
except ImportError as e:
|
||||
print(f' ❌ bcrypt import failed: {e}')
|
||||
except Exception as e:
|
||||
print(f' ❌ bcrypt error: {e}')
|
||||
"
|
||||
else
|
||||
echo " ⚠️ Please activate virtual environment first"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "4. Library Files Check:"
|
||||
if [ -d "req_libraries_32bit" ]; then
|
||||
echo " ✅ req_libraries_32bit exists"
|
||||
if ls req_libraries_32bit/bcrypt*.whl >/dev/null 2>&1; then
|
||||
BCRYPT_FILE=$(ls req_libraries_32bit/bcrypt*.whl | head -1)
|
||||
echo " 32-bit bcrypt: $(basename $BCRYPT_FILE)"
|
||||
|
||||
# Quick check of wheel architecture
|
||||
echo " Checking wheel architecture..."
|
||||
unzip -l "$BCRYPT_FILE" | grep "\.so" | head -1 || echo " No .so files found"
|
||||
else
|
||||
echo " ❌ No bcrypt wheel found in 32-bit libraries"
|
||||
fi
|
||||
else
|
||||
echo " ❌ req_libraries_32bit folder missing"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "5. Recommendations:"
|
||||
echo " • Ensure you're on 32-bit Raspberry Pi OS (armv7l)"
|
||||
echo " • Use: source .venv/bin/activate"
|
||||
echo " • Run: ./install_32bit.sh for clean installation"
|
||||
echo " • If still failing, remove .venv and reinstall"
|
||||
@@ -1,11 +0,0 @@
|
||||
Video Profile for Player Compatibility (based on intro1.mp4)
|
||||
|
||||
Codec: H.264
|
||||
Profile: Main
|
||||
Resolution: 1920x1080
|
||||
Bitrate: ~14,700 kbps
|
||||
Framerate: 29.97 fps
|
||||
|
||||
Recommended ffmpeg conversion command:
|
||||
|
||||
ffmpeg -i input.mp4 -c:v libx264 -profile:v main -b:v 14700k -vf "scale=1920:1080,fps=29.97" -c:a copy output_normalized.mp4
|
||||
Reference in New Issue
Block a user