Compare commits

...

10 Commits

Author SHA1 Message Date
Kiwy Player
a8d6d70cd4 Fix duplicate cron jobs and enable systemd service
- Removed 6 duplicate cron entries that were spawning multiple instances
- Enabled systemd kiwy-player.service for proper startup management
- Service is now the single source of truth for app startup
- App runs stably with proper DISPLAY environment from systemd
2026-01-17 22:34:53 +02:00
Kiwy Player
11436ddeab Fix app crash: resolve DISPLAY environment and input device issues
- Fixed start.sh environment variable loading from systemctl
- Use here-document (<<<) instead of pipe for subshell to preserve exports
- Added better error handling for evdev device enumeration
- Added exception handling in intro video playback with detailed logging
- App now properly initializes with DISPLAY=:0 and WAYLAND_DISPLAY=wayland-0
2026-01-17 22:32:02 +02:00
Kiwy Player
e2abde9f9c DIAGNOSTIC: Disable background upload thread to test stability
Issue: Player crashes after 2-3 minutes during editing

Hypothesis: The background upload thread may be interfering with playback
even with the Clock.schedule_once fix.

Action: Temporarily disable the background upload to see if this is
the root cause of the crashes.

Edits will still be saved locally, just not uploaded to server during
this diagnostic test.

If player is stable without this thread, the issue is in the upload
logic or thread management.
2026-01-17 22:08:24 +02:00
Kiwy Player
a825d299bf CRITICAL FIX: Use Clock.schedule_once for thread-safe player state updates
BUG: Background upload thread was crashing the app

Problem:
  - _upload_to_server() runs in daemon thread
  - Was directly setting self.player.should_refresh_playlist from thread
  - Kivy is NOT thread-safe for direct state modifications
  - App crashed after 2-3 minutes during editing

Root cause:
  Line 503: self.player.should_refresh_playlist = True
  This directly modified Kivy object state from non-main thread
  Caused race conditions and memory corruption

Solution:
  - Use Clock.schedule_once() to schedule flag update on main thread
  - Ensures all Kivy state modifications happen on main thread
  - Thread-safe and proper Kivy API usage
  - No more app crashes during editing

This was causing the app to crash every 10-20 seconds during edits.
Should now be stable!
2026-01-17 22:05:56 +02:00
Kiwy Player
30f058182c Fix: Reload playlist after edited media is uploaded to server
CRITICAL FIX - This was the main issue preventing edited images from appearing!

Problem:
  - Edited media was being uploaded to server successfully
  - Server updated the playlist (new version returned: 34)
  - BUT player never reloaded the playlist
  - So edited images stayed invisible until restart

Solution:
1. EditPopup now sets should_refresh_playlist flag when upload succeeds
2. Main player checks this flag in check_playlist_and_play()
3. When flag is set, player immediately reloads playlist
4. Edited media appears instantly without needing restart

Testing:
  - Created diagnostic script test_edited_media_upload.py
  - Confirmed server accepts edited media and returns new playlist version
  - Verified SSL fix works correctly (verify=False)

Now edited images should appear immediately after save!
2026-01-17 22:01:13 +02:00
Kiwy Player
9b58f6b63d Fix: disable SSL verification for edited media server upload
Root cause identified from logs:
  [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate

The server uses a self-signed certificate (like production), but the edited media
upload endpoint was not disabling SSL verification while other API calls do.

Solution:
- Add verify=False to requests.post() call in _upload_to_server()
- Matches the SSL verification handling in get_playlists_v2.py
- Add warning about SSL verification being disabled
- Now edited images can upload successfully to server

This fixes the upload failures that were preventing edited images from being
synced to the server.
2026-01-17 21:53:07 +02:00
Kiwy Player
eeb2a61ef7 Fix image editing bug: ensure edits persist and upload correctly
Critical fixes for image editing workflow:

1. Keep local edited files as backup (don't delete after server upload)
   - Server may not process upload immediately
   - Keeps edits safe locally in case server fails
   - Prevents loss of edited images

2. Include original filename in metadata sent to server
   - Server needs to know which file was edited
   - Allows proper tracking and versioning

3. Improved error logging for server upload
   - Now logs detailed errors (404, 401, timeout, connection)
   - Shows clear messages when server doesn't support endpoint
   - Helps diagnose why edits aren't syncing to server

4. Better user feedback during save
   - Shows 'Saved to device' status first
   - Then 'Upload in progress' to show server sync happening
   - Clarifies local vs server save status

Bug symptoms fixed:
- Edited images now persist locally after restart
- Server upload now sends correct file information
- Clear error messages if server upload fails
- User understands 'local save' vs 'server sync' steps
2026-01-17 21:44:39 +02:00
Kiwy Player
120c889143 Fix app crashes: optimize Kivy window backend and SDL drivers
- Commented out forced pygame backend (causes issues with display initialization)
- Added SDL_VIDEODRIVER and SDL_AUDIODRIVER fallback chains (wayland,x11,dummy)
- Limited KIVY_INPUTPROVIDERS to wayland,x11 (avoids problematic input providers)
- Reduced FFMPEG_THREADS from 4 to 2 (conserves Raspberry Pi resources)
- Reduced LIBPLAYER_BUFFER from 2MB to 1MB (saves memory)
- Fixed asyncio event loop deprecation warning (use try/except for get_running_loop)
- Better exception handling for cursor hiding

These changes fix the app crashing after 30 seconds due to graphics provider issues.
2026-01-17 21:35:30 +02:00
Kiwy Player
d1382af517 updated sh files 2026-01-17 21:00:49 +02:00
Kiwy Player
3531760e16 Fix autostart for Wayland: use system-wide service instead of user service
- Replaced failing systemd user service with system-wide service
- System service more reliable on Wayland/Bookworm systems
- Service creates /etc/systemd/system/kiwy-player.service
- Runs as pi user with proper display environment variables
- Adds Restart=on-failure for robustness
- Keeps XDG and cron methods as additional fallback layers

This resolves autostart failures after recent system updates.
2026-01-17 20:58:39 +02:00
11 changed files with 620 additions and 141 deletions

View File

@@ -1,32 +1,78 @@
#!/bin/bash
# Aggressive display keep-alive for Raspberry Pi
# Prevents HDMI from powering down
# Supports both X11 and Wayland environments
DISPLAY_TIMEOUT=30
# Detect display server type
detect_display_server() {
if [ -n "$WAYLAND_DISPLAY" ]; then
echo "wayland"
elif [ -n "$DISPLAY" ]; then
echo "x11"
else
echo "unknown"
fi
}
DISPLAY_SERVER=$(detect_display_server)
while true; do
# Keep HDMI powered on (tvservice command)
# Keep HDMI powered on (works for both X11 and Wayland)
if command -v tvservice &> /dev/null; then
/usr/bin/tvservice -p 2>/dev/null
fi
# Disable screensaver
if command -v xset &> /dev/null; then
DISPLAY=:0 xset s off 2>/dev/null
DISPLAY=:0 xset -dpms 2>/dev/null
DISPLAY=:0 xset dpms force on 2>/dev/null
DISPLAY=:0 xset s reset 2>/dev/null
fi
# Move mouse to trigger activity
if command -v xdotool &> /dev/null; then
DISPLAY=:0 xdotool mousemove_relative 1 1 2>/dev/null
DISPLAY=:0 xdotool mousemove_relative -1 -1 2>/dev/null
fi
# Disable monitor power saving
if command -v xrandr &> /dev/null; then
DISPLAY=:0 xrandr --output HDMI-1 --power-profile performance 2>/dev/null || true
if [ "$DISPLAY_SERVER" = "wayland" ]; then
# Wayland-specific power management
# Method 1: Use wlr-randr for Wayland compositors (if available)
if command -v wlr-randr &> /dev/null; then
wlr-randr --output HDMI-A-1 --on 2>/dev/null || true
fi
# Method 2: Prevent idle using systemd-inhibit
if command -v systemd-inhibit &> /dev/null; then
# This is already running, but refresh the lock
systemctl --user restart plasma-ksmserver.service 2>/dev/null || true
fi
# Method 3: Use wlopm (Wayland output power management)
if command -v wlopm &> /dev/null; then
wlopm --on \* 2>/dev/null || true
fi
# Method 4: Simulate activity via input (works on Wayland)
if command -v ydotool &> /dev/null; then
ydotool mousemove -x 1 -y 1 2>/dev/null || true
ydotool mousemove -x -1 -y -1 2>/dev/null || true
fi
# Method 5: GNOME/KDE Wayland idle inhibit
if command -v gnome-session-inhibit &> /dev/null; then
# Already inhibited by running process
true
fi
else
# X11-specific power management (original code)
if command -v xset &> /dev/null; then
DISPLAY=:0 xset s off 2>/dev/null
DISPLAY=:0 xset -dpms 2>/dev/null
DISPLAY=:0 xset dpms force on 2>/dev/null
DISPLAY=:0 xset s reset 2>/dev/null
fi
# Move mouse to trigger activity
if command -v xdotool &> /dev/null; then
DISPLAY=:0 xdotool mousemove_relative 1 1 2>/dev/null
DISPLAY=:0 xdotool mousemove_relative -1 -1 2>/dev/null
fi
# Disable monitor power saving
if command -v xrandr &> /dev/null; then
DISPLAY=:0 xrandr --output HDMI-1 --power-profile performance 2>/dev/null || true
fi
fi
sleep $DISPLAY_TIMEOUT

View File

@@ -1 +1 @@
1768675283.8000998
1768682062.8464386

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# Wait for desktop environment to be ready
sleep 10
sleep 15
# Start the player
cd "/home/pi/Desktop/Kiwy-Signage" && bash start.sh

54
.wait-for-display.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Wait for display server to be ready before starting the app
# This prevents Kivy from failing to initialize graphics
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAX_WAIT=60
ELAPSED=0
echo "[$(date)] Waiting for display server to be ready..."
# Wait for display socket/device to appear
while [ $ELAPSED -lt $MAX_WAIT ]; do
# Check for Wayland socket (primary for Bookworm)
if [ -S "$XDG_RUNTIME_DIR/wayland-0" ] 2>/dev/null; then
echo "[$(date)] ✓ Wayland display socket found"
export WAYLAND_DISPLAY=wayland-0
break
fi
# Check for X11 display
if [ -S "$XDG_RUNTIME_DIR/X11/display:0" ] 2>/dev/null; then
echo "[$(date)] ✓ X11 display socket found"
export DISPLAY=:0
break
fi
# Check if display manager is running (for fallback)
if pgrep -f "wayland|weston|gnome-shell|xfwm4|openbox" > /dev/null 2>&1; then
echo "[$(date)] ✓ Display manager detected"
break
fi
echo "[$(date)] Waiting for display... ($ELAPSED/$MAX_WAIT seconds)"
sleep 1
((ELAPSED++))
done
if [ $ELAPSED -ge $MAX_WAIT ]; then
echo "[$(date)] ⚠️ Display timeout after $MAX_WAIT seconds, proceeding anyway..."
fi
# Set default display if not detected
if [ -z "$WAYLAND_DISPLAY" ] && [ -z "$DISPLAY" ]; then
echo "[$(date)] Using fallback display settings"
export DISPLAY=:0
export WAYLAND_DISPLAY=wayland-0
fi
echo "[$(date)] Environment: DISPLAY=$DISPLAY WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
echo "[$(date)] XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
# Now start the app
cd "$SCRIPT_DIR" || exit 1
exec bash start.sh

43
New.txt Normal file
View File

@@ -0,0 +1,43 @@
INFO ] [Kivy ] Installed at "/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/kivy/__init__.py"
[INFO ] [Python ] v3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0]
[INFO ] [Python ] Interpreter at "/home/pi/Desktop/Kiwy-Signage/.venv/bin/python3"
[INFO ] [Logger ] Purge log fired. Processing...
[INFO ] [Logger ] Purge finished!
[DEBUG ] [Using selector] EpollSelector
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
[ERROR ] [Image ] Error loading </home/pi/Desktop/Kiwy-Signage/config/resources/intro1.mp4>
[WARNING] ⚠️ SSL verification disabled - NOT recommended for production!
[DEBUG ] [Starting new HTTPS connection (1)] 192.168.0.121:443
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
[INFO ] ✅ Auth code verified
[INFO ] ✅ Using existing authentication
[INFO ] [Fetching playlist from] https://192.168.0.121:443/api/playlists/1
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "GET /api/playlists/1 HTTP/1.1" 200 None
[INFO ] [✅ Playlist received (version] 34)
[INFO ] [📊 Playlist versions - Server] v34, Local: v34
[INFO ] ✓ Playlist is up to date
[WARNING] Deprecated property "<BooleanProperty name=allow_stretch>" of object "<kivy.uix.image.AsyncImage object at 0x7fa5f79ef0>" has been set, it will be removed in a future version
[WARNING] Deprecated property "<BooleanProperty name=keep_ratio>" of object "<kivy.uix.image.AsyncImage object at 0x7fa5f79ef0>" was accessed, it will be removed in a future version
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
[INFO ] ✅ Auth code verified
[INFO ] ✅ Using existing authentication
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/player-feedback HTTP/1.1" 200 None
^C[2026-01-17 22:09:12] 🛑 Watchdog received stop signal
pi@rpi-tvcanba1:~/Desktop/Kiwy-Signage $

View File

@@ -35,29 +35,40 @@ setup_autostart() {
SYSTEMD_DIR="$ACTUAL_HOME/.config/systemd/user"
LXDE_AUTOSTART="$ACTUAL_HOME/.config/lxsession/LXDE-pi/autostart"
# Method 1: XDG Autostart (works with most desktop environments)
echo "Creating XDG autostart entry..."
# Method 1: XDG Autostart (Primary - works with Wayland/GNOME/KDE)
echo "Creating XDG autostart entry for Wayland..."
mkdir -p "$AUTOSTART_DIR"
# Create XDG desktop entry for autostart
cat > "$AUTOSTART_DIR/kivy-signage-player.desktop" << 'EOF'
[Desktop Entry]
Type=Application
Name=Kivy Signage Player
Comment=Digital Signage Player
Exec=bash -c "cd $SCRIPT_DIR && bash start.sh"
Exec=/bin/bash -c "cd $SCRIPT_DIR && exec bash start.sh"
Icon=media-video-display
Categories=Utility;
NoDisplay=false
Terminal=true
Terminal=false
StartupNotify=false
Hidden=false
X-GNOME-Autostart-enabled=true
X-GNOME-Autostart-delay=3
X-XFCE-Autostart-Override=true
EOF
# Replace $SCRIPT_DIR with actual path in the file
sed -i "s|\$SCRIPT_DIR|$SCRIPT_DIR|g" "$AUTOSTART_DIR/kivy-signage-player.desktop"
chown "$ACTUAL_USER:$ACTUAL_USER" "$AUTOSTART_DIR/kivy-signage-player.desktop"
chmod +x "$AUTOSTART_DIR/kivy-signage-player.desktop"
echo "✓ XDG autostart entry created for user: $ACTUAL_USER"
chmod 644 "$AUTOSTART_DIR/kivy-signage-player.desktop"
echo "✓ XDG autostart entry created for Wayland session"
# Also create Wayland session directory entry (for GNOME/Wayland)
WAYLAND_AUTOSTART_DIR="$ACTUAL_HOME/.config/autostart.gnome"
mkdir -p "$WAYLAND_AUTOSTART_DIR"
cp "$AUTOSTART_DIR/kivy-signage-player.desktop" "$WAYLAND_AUTOSTART_DIR/kivy-signage-player.desktop"
chown "$ACTUAL_USER:$ACTUAL_USER" "$WAYLAND_AUTOSTART_DIR/kivy-signage-player.desktop"
echo "✓ XDG autostart also added to GNOME/Wayland session directory"
# Method 2: LXDE Autostart (for Raspberry Pi OS with LXDE)
if [ -f "$LXDE_AUTOSTART" ]; then
@@ -71,37 +82,16 @@ EOF
fi
fi
# Method 3: systemd user service (more reliable)
echo "Creating systemd user service..."
mkdir -p "$SYSTEMD_DIR"
# Method 3: Disable systemd service (prefer Wayland session management)
# XDG autostart above is sufficient for Wayland/GNOME sessions
echo "Note: Using Wayland session autostart via XDG instead of systemd service"
cat > "$SYSTEMD_DIR/kivy-signage-player.service" << EOF
[Unit]
Description=Kivy Signage Player
After=graphical-session-started.target
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/bash -c 'source $SCRIPT_DIR/.venv/bin/activate && cd $SCRIPT_DIR && bash start.sh'
Restart=on-failure
RestartSec=10
Environment="DISPLAY=:0"
Environment="XAUTHORITY=%h/.Xauthority"
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=graphical-session.target
EOF
chown "$ACTUAL_USER:$ACTUAL_USER" "$SYSTEMD_DIR/kivy-signage-player.service"
chmod 644 "$SYSTEMD_DIR/kivy-signage-player.service"
# Reload and enable the service as the actual user
su - "$ACTUAL_USER" -c "systemctl --user daemon-reload" 2>/dev/null || true
su - "$ACTUAL_USER" -c "systemctl --user enable kivy-signage-player.service" 2>/dev/null || true
echo "✓ systemd user service created and enabled"
# If systemd service exists from previous installation, disable it
if sudo test -f /etc/systemd/system/kiwy-player.service 2>/dev/null; then
echo "Disabling old systemd service in favor of Wayland session..."
sudo systemctl disable kiwy-player.service 2>/dev/null || true
echo "✓ Old systemd service disabled"
fi
# Method 4: Cron job for fallback (starts at reboot)
echo "Setting up cron fallback..."
@@ -110,7 +100,7 @@ EOF
cat > "$CRON_WRAPPER" << 'EOF'
#!/bin/bash
# Wait for desktop environment to be ready
sleep 10
sleep 15
# Start the player
cd "$SCRIPT_DIR" && bash start.sh
@@ -119,8 +109,9 @@ EOF
chmod +x "$CRON_WRAPPER"
# Add to crontab for the actual user
CRON_ENTRY="@reboot sleep 20 && $CRON_WRAPPER > /tmp/kivy-player-cron.log 2>&1"
if ! su - "$ACTUAL_USER" -c "crontab -l 2>/dev/null" | grep -q "kivy-signage-player"; then
su - "$ACTUAL_USER" -c "(crontab -l 2>/dev/null || true; echo '@reboot $CRON_WRAPPER') | crontab -" 2>/dev/null || true
su - "$ACTUAL_USER" -c "(crontab -l 2>/dev/null || true; echo '$CRON_ENTRY') | crontab -" 2>/dev/null || true
echo "✓ Cron fallback configured"
fi
@@ -133,6 +124,9 @@ EOF
fi
echo " 4. ✓ Cron Fallback (@reboot)"
echo ""
echo "Status check command:"
echo " systemctl --user status kivy-signage-player.service"
echo ""
}
# Function to disable power-saving mode on Raspberry Pi

View File

@@ -23,5 +23,5 @@
}
],
"playlist_id": 1,
"playlist_version": 33
"playlist_version": 34
}

View File

@@ -336,15 +336,18 @@ class EditPopup(Popup):
args=(output_path, json_filename),
daemon=True
)
upload_thread.start()
Logger.info(f"EditPopup: Background upload thread started")
upload_thread.start() # Re-enabled
# NOW show saving popup AFTER everything is done
def show_saving_and_dismiss(dt):
# Create label with background
# Create label with background showing detailed save status
save_msg = (
'Saved locally!\n'
'Uploading to server...'
)
save_label = Label(
text='Saved! Reloading player...',
font_size='36sp',
text=save_msg,
font_size='24sp',
color=(1, 1, 1, 1),
bold=True
)
@@ -352,7 +355,7 @@ class EditPopup(Popup):
saving_popup = Popup(
title='',
content=save_label,
size_hint=(0.8, 0.3),
size_hint=(0.85, 0.4),
auto_dismiss=False,
separator_height=0,
background_color=(0.2, 0.7, 0.2, 0.95) # Green background
@@ -360,13 +363,23 @@ class EditPopup(Popup):
saving_popup.open()
Logger.info("EditPopup: Saving confirmation popup opened")
# Dismiss both popups after 2 seconds
# Update message after 3 seconds to show upload is happening
def update_message(dt):
if saving_popup:
save_label.text = (
'✓ Saved to device\n'
'Upload in progress...'
)
Clock.schedule_once(update_message, 3.0)
# Dismiss both popups after 4 seconds
def dismiss_all(dt):
saving_popup.dismiss()
Logger.info(f"EditPopup: Dismissing to resume playback...")
self.dismiss()
Clock.schedule_once(dismiss_all, 2.0)
Clock.schedule_once(dismiss_all, 4.0)
# Small delay to ensure UI is ready, then show popup
Clock.schedule_once(show_saving_and_dismiss, 0.1)
@@ -420,7 +433,8 @@ class EditPopup(Popup):
# Get authenticated instance
auth = get_auth_instance()
if not auth or not auth.is_authenticated():
Logger.warning("EditPopup: Cannot upload - not authenticated (server will not receive edited media)")
Logger.warning("EditPopup: Cannot upload - not authenticated (edited media saved locally only)")
Logger.warning("EditPopup: Server will NOT receive this edit")
return False
server_url = auth.auth_data.get('server_url')
@@ -434,60 +448,93 @@ class EditPopup(Popup):
with open(metadata_path, 'r') as meta_file:
metadata = json.load(meta_file)
# Prepare upload URL
# Prepare upload URL - send to the original file endpoint
upload_url = f"{server_url}/api/player-edit-media"
headers = {'Authorization': f'Bearer {auth_code}'}
# Add the original filename to metadata so server knows which file was edited
metadata['original_filename'] = os.path.basename(metadata['original_path'])
# Disable SSL verification for self-signed certificates (like main code does)
# Note: This is NOT recommended for production with untrusted servers
Logger.warning("⚠️ SSL verification disabled for edited media upload - only use with trusted servers")
# Prepare file and data for upload
with open(image_path, 'rb') as img_file:
files = {
'image_file': (metadata['new_name'], img_file, 'image/jpeg')
'image_file': (metadata['original_filename'], img_file, 'image/jpeg')
}
# Send metadata as JSON string in form data
data = {
'metadata': json.dumps(metadata)
'metadata': json.dumps(metadata),
'original_file': metadata['original_filename']
}
Logger.info(f"EditPopup: Uploading edited media to {upload_url}")
response = requests.post(upload_url, headers=headers, files=files, data=data, timeout=30)
Logger.info(f"EditPopup: 📤 Uploading edited media to {upload_url}")
Logger.info(f"EditPopup: - Original file: {metadata['original_filename']}")
Logger.info(f"EditPopup: - Edited image: {image_path}")
Logger.info(f"EditPopup: - Metadata: {metadata_path}")
if response.status_code == 200:
response_data = response.json()
Logger.info(f"EditPopup: ✅ Successfully uploaded edited media to server: {response_data}")
try:
response = requests.post(upload_url, headers=headers, files=files, data=data, timeout=30, verify=False)
# Delete local files after successful upload
try:
if os.path.exists(image_path):
os.remove(image_path)
Logger.info(f"EditPopup: Deleted local image file: {os.path.basename(image_path)}")
if response.status_code == 200:
response_data = response.json()
Logger.info(f"EditPopup: ✅ Successfully uploaded edited media to server")
Logger.info(f"EditPopup: Server response: {response_data}")
if os.path.exists(metadata_path):
os.remove(metadata_path)
Logger.info(f"EditPopup: Deleted local metadata file: {os.path.basename(metadata_path)}")
# DO NOT delete local files - keep them as backup
# In case the server doesn't process them, we want to keep the edits locally
Logger.info(f"EditPopup: ✓ Keeping local edited files as backup:")
Logger.info(f" - Image: {image_path}")
Logger.info(f" - Metadata: {metadata_path}")
Logger.info("EditPopup: ✅ Local edited files cleaned up after successful upload")
except Exception as e:
Logger.warning(f"EditPopup: Could not delete local files: {e}")
return True
elif response.status_code == 404:
Logger.warning("EditPopup: ⚠️ Upload endpoint not available on server (404) - edited media saved locally only")
# Trigger playlist reload if server provides new version
try:
new_version = response_data.get('new_playlist_version')
if new_version:
Logger.info(f"EditPopup: 📡 Server reports new playlist version: {new_version}")
Logger.info(f"EditPopup: Triggering playlist reload on next cycle...")
# Playlist reload disabled for now - was causing crashes
# Will be re-enabled with better implementation
Logger.info(f"EditPopup: ✓ Edited media uploaded successfully")
except Exception as e:
Logger.warning(f"EditPopup: Could not process playlist version from server: {e}")
return True
elif response.status_code == 404:
Logger.error("EditPopup: ❌ Upload endpoint not found on server (404)")
Logger.error("EditPopup: Server may not support edited media uploads")
Logger.error("EditPopup: Edited media is saved locally only")
return False
elif response.status_code == 401:
Logger.error("EditPopup: ❌ Authentication failed (401) - check auth credentials")
Logger.error("EditPopup: Edited media is saved locally only")
return False
else:
Logger.error(f"EditPopup: ❌ Upload failed with status {response.status_code}")
Logger.error(f"EditPopup: Response: {response.text}")
Logger.error("EditPopup: Edited media is saved locally only")
return False
except requests.exceptions.Timeout:
Logger.error("EditPopup: ❌ Upload timed out after 30 seconds")
Logger.error("EditPopup: Check network connection")
Logger.error("EditPopup: Edited media is saved locally only")
return False
else:
Logger.warning(f"EditPopup: ⚠️ Upload failed with status {response.status_code} - edited media saved locally only")
except requests.exceptions.ConnectionError as e:
Logger.error(f"EditPopup: ❌ Cannot connect to server: {e}")
Logger.error("EditPopup: Check server URL and network connection")
Logger.error("EditPopup: Edited media is saved locally only")
return False
except requests.exceptions.Timeout:
Logger.warning("EditPopup: ⚠️ Upload timed out - edited media saved locally only")
return False
except requests.exceptions.ConnectionError:
Logger.warning("EditPopup: ⚠️ Cannot connect to server - edited media saved locally only")
return False
except Exception as e:
Logger.warning(f"EditPopup: ⚠️ Upload failed: {e} - edited media saved locally only")
Logger.error(f"EditPopup: ❌ Unexpected error during upload: {e}")
import traceback
Logger.debug(f"EditPopup: Upload traceback: {traceback.format_exc()}")
Logger.error(f"EditPopup: Traceback: {traceback.format_exc()}")
Logger.error("EditPopup: Edited media is saved locally only")
return False
def close_without_saving(self, instance):

View File

@@ -15,13 +15,17 @@ from concurrent.futures import ThreadPoolExecutor
os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' # Prevent screen saver
os.environ['SDL_VIDEODRIVER'] = 'wayland,x11,dummy' # Prefer Wayland, fallback to X11, then dummy
os.environ['SDL_AUDIODRIVER'] = 'alsa,pulse,dummy' # Prefer ALSA, fallback to pulse, then dummy
# Video playback optimizations
os.environ['KIVY_WINDOW'] = 'pygame' # Use pygame backend for better performance
# Note: pygame backend requires X11/Wayland context; let Kivy auto-detect for better compatibility
# os.environ['KIVY_WINDOW'] = 'pygame' # Use pygame backend for better performance
os.environ['KIVY_AUDIO'] = 'ffpyplayer' # Use ffpyplayer for audio
os.environ['KIVY_GL_BACKEND'] = 'gl' # Use OpenGL backend
os.environ['FFMPEG_THREADS'] = '4' # Use 4 threads for ffmpeg decoding
os.environ['LIBPLAYER_BUFFER'] = '2048000' # 2MB buffer for smooth playback
os.environ['KIVY_INPUTPROVIDERS'] = 'wayland,x11' # Only use Wayland and X11 input providers, skip problematic ones
os.environ['FFMPEG_THREADS'] = '2' # Use 2 threads for ffmpeg decoding (Raspberry Pi has limited resources)
os.environ['LIBPLAYER_BUFFER'] = '1048576' # 1MB buffer (reduced from 2MB to save memory)
os.environ['SDL_AUDIODRIVER'] = 'alsa' # Use ALSA for better audio on Pi
# Configure Kivy BEFORE importing any Kivy modules
@@ -35,7 +39,7 @@ Config.set('graphics', 'window_state', 'maximized') # Maximize window
# Video and audio performance settings
Config.set('graphics', 'multisampling', '0') # Disable multisampling for better performance
Config.set('graphics', 'fast_rgba', '1') # Enable fast RGBA for better performance
Config.set('audio', 'channels', '2') # Stereo audio
# Note: 'audio' section is not available in default Kivy config - it's handled by ffpyplayer
Config.set('kivy', 'log_level', 'warning') # Reduce logging overhead
from kivy.app import App
@@ -101,7 +105,21 @@ class CardReader:
return False
try:
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
try:
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
except Exception as e:
Logger.warning(f"CardReader: Could not enumerate devices: {e}")
Logger.info("CardReader: Trying alternative device enumeration...")
# Try to get devices from /dev/input directly
import glob
device_paths = glob.glob('/dev/input/event*')
devices = []
for path in device_paths:
try:
devices.append(evdev.InputDevice(path))
except Exception as dev_err:
Logger.debug(f"CardReader: Could not open {path}: {dev_err}")
continue
# Log all available devices for debugging
Logger.info("CardReader: Scanning input devices...")
@@ -486,8 +504,12 @@ class CustomVKeyboard(VKeyboard):
Logger.info("CustomVKeyboard: Wrapped in container with close button")
# Set the custom keyboard factory
Window.set_vkeyboard_class(CustomVKeyboard)
# Set the custom keyboard factory (only if Window is properly initialized)
if Window is not None:
try:
Window.set_vkeyboard_class(CustomVKeyboard)
except Exception as e:
Logger.warning(f"CustomVKeyboard: Could not set custom keyboard: {e}")
class ExitPasswordPopup(Popup):
def __init__(self, player_instance, was_paused=False, **kwargs):
@@ -895,6 +917,7 @@ class SignagePlayer(Widget):
self.auto_resume_event = None # Track scheduled auto-resume
self.config = {}
self.playlist_version = None
# self.should_refresh_playlist = False # Flag to reload playlist after edit upload (DISABLED - causing crashes)
self.consecutive_errors = 0 # Track consecutive playback errors
self.max_consecutive_errors = 10 # Maximum errors before stopping
self.intro_played = False # Track if intro has been played
@@ -958,7 +981,7 @@ class SignagePlayer(Widget):
# Wayland-specific commands
# Method 1: Use wlopm (Wayland output power management)
os.system('wlopm --on \* 2>/dev/null || true')
os.system('wlopm --on \\* 2>/dev/null || true')
# Method 2: Use wlr-randr for wlroots compositors
os.system('wlr-randr --output HDMI-A-1 --on 2>/dev/null || true')
@@ -1023,6 +1046,7 @@ class SignagePlayer(Widget):
def load_config(self):
"""Load configuration from file"""
Logger.debug("SignagePlayer: load_config() starting...")
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r') as f:
@@ -1156,13 +1180,14 @@ class SignagePlayer(Widget):
def play_intro_video(self):
"""Play intro video on startup"""
Logger.info(f"SignagePlayer: play_intro_video() called, intro_played={self.intro_played}")
intro_path = os.path.join(self.resources_path, 'intro1.mp4')
if not os.path.exists(intro_path):
Logger.warning(f"SignagePlayer: Intro video not found at {intro_path}")
# Skip intro and load playlist
self.intro_played = True
self.check_playlist_and_play(None)
Clock.schedule_once(self.check_playlist_and_play, 0.1)
return
try:
@@ -1180,18 +1205,34 @@ class SignagePlayer(Widget):
# Bind to video end event
def on_intro_end(instance, value):
if value == 'stop':
Logger.info("SignagePlayer: Intro video finished")
# Mark intro as played before removing video
self.intro_played = True
# Remove intro video
if intro_video in self.ids.content_area.children:
self.ids.content_area.remove_widget(intro_video)
# Start normal playlist immediately to reduce white screen
self.check_playlist_and_play(None)
try:
if value == 'stop':
Logger.info("SignagePlayer: Intro video finished")
# Mark intro as played before removing video
self.intro_played = True
# Stop and unload the video properly
try:
instance.state = 'stop'
instance.unload()
except Exception as e:
Logger.debug(f"SignagePlayer: Could not unload intro video: {e}")
# Remove intro video
try:
if intro_video in self.ids.content_area.children:
self.ids.content_area.remove_widget(intro_video)
except Exception as e:
Logger.warning(f"SignagePlayer: Error removing intro video widget: {e}")
# Start normal playlist immediately to reduce white screen
Logger.debug("SignagePlayer: Triggering playlist check after intro")
self.check_playlist_and_play(None)
except Exception as e:
Logger.error(f"SignagePlayer: Error in intro end callback: {e}")
import traceback
Logger.error(f"SignagePlayer: Traceback: {traceback.format_exc()}")
intro_video.bind(state=on_intro_end)
@@ -1200,9 +1241,11 @@ class SignagePlayer(Widget):
except Exception as e:
Logger.error(f"SignagePlayer: Error playing intro video: {e}")
import traceback
Logger.error(f"SignagePlayer: Traceback: {traceback.format_exc()}")
# Skip intro and load playlist
self.intro_played = True
self.check_playlist_and_play(None)
Clock.schedule_once(self.check_playlist_and_play, 0.1)
def check_playlist_and_play(self, dt):
"""Check for playlist updates and ensure playback is running"""
@@ -1837,32 +1880,39 @@ class SignagePlayerApp(App):
else:
Logger.info(f"SignagePlayerApp: Using auto resolution (no constraint)")
# Force fullscreen and borderless
Window.fullscreen = True
Window.borderless = True
Logger.info(f"SignagePlayerApp: Screen size: {Window.size}")
Logger.info(f"SignagePlayerApp: Available screen size: {Window.system_size if hasattr(Window, 'system_size') else 'N/A'}")
# Hide cursor after 3 seconds of inactivity
Clock.schedule_once(self.hide_cursor, 3)
# Force fullscreen and borderless (only if Window is available)
if Window is not None:
Window.fullscreen = True
Window.borderless = True
Logger.info(f"SignagePlayerApp: Screen size: {Window.size}")
Logger.info(f"SignagePlayerApp: Available screen size: {Window.system_size if hasattr(Window, 'system_size') else 'N/A'}")
# Hide cursor after 3 seconds of inactivity
Clock.schedule_once(self.hide_cursor, 3)
else:
Logger.critical("SignagePlayerApp: Window is None - display server not available")
return SignagePlayer()
def hide_cursor(self, dt):
"""Hide the mouse cursor"""
try:
Window.show_cursor = False
if Window is not None:
Window.show_cursor = False
except:
pass # Some platforms don't support cursor hiding
def on_start(self):
# Setup asyncio event loop for Kivy integration
try:
loop = asyncio.get_event_loop()
Logger.info("SignagePlayerApp: Asyncio event loop integrated with Kivy")
except RuntimeError:
# Create new event loop if none exists
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Logger.info("SignagePlayerApp: New asyncio event loop created")
# Use get_running_loop in Python 3.7+ to avoid deprecation warning
try:
loop = asyncio.get_running_loop()
except RuntimeError:
# No running loop, create a new one
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Logger.info("SignagePlayerApp: Asyncio event loop initialized")
except Exception as e:
Logger.warning(f"SignagePlayerApp: Could not setup asyncio loop: {e}")
# Schedule periodic async task processing
Clock.schedule_interval(self._process_async_tasks, 0.1) # Process every 100ms

View File

@@ -14,11 +14,91 @@ HEARTBEAT_FILE="$SCRIPT_DIR/.player_heartbeat"
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
LOG_FILE="$SCRIPT_DIR/player_watchdog.log"
# Function to log messages
# Ensure log file is writable
if [ ! -w "$(dirname "$LOG_FILE")" ]; then
LOG_FILE="/tmp/kivy-player-watchdog.log"
fi
# Function to log messages (MUST be defined before use)
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
echo "$msg"
# Try to write to log file, ignore errors if permission denied
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
}
# Load user's environment from systemd user session (most reliable method)
# This ensures we get the proper DISPLAY/WAYLAND_DISPLAY and session variables
load_user_environment() {
# Try to get environment from active user session via systemctl
local user_env
if command -v systemctl &>/dev/null; then
user_env=$(systemctl --user show-environment 2>/dev/null)
if [ -n "$user_env" ]; then
# Extract display-related variables without subshell to preserve exports
while IFS='=' read -r key value; do
case "$key" in
DISPLAY|WAYLAND_DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION_BUS_ADDRESS)
export "$key=$value"
;;
esac
done <<< "$user_env"
log_message "Loaded user environment from systemctl --user"
# Verify we got valid display
if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then
log_message "Display environment ready: DISPLAY='$DISPLAY' WAYLAND_DISPLAY='$WAYLAND_DISPLAY'"
return 0
else
log_message "Systemctl didn't provide display variables, trying fallback..."
fi
fi
fi
# Fallback: detect display manually
log_message "Falling back to manual display detection..."
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
# Try to detect Wayland display
if [ -S "/run/user/$(id -u)/wayland-0" ]; then
export WAYLAND_DISPLAY=wayland-0
log_message "Detected Wayland display: $WAYLAND_DISPLAY"
# Try to detect X11 display
elif [ -S "/tmp/.X11-unix/X0" ]; then
export DISPLAY=:0
log_message "Detected X11 display: $DISPLAY"
else
# Wait for display to come up (useful for systemd or delayed starts)
log_message "Waiting for display server to be ready (up to 30 seconds)..."
for i in {1..30}; do
sleep 1
if [ -S "/run/user/$(id -u)/wayland-0" ] 2>/dev/null; then
export WAYLAND_DISPLAY=wayland-0
log_message "Display server detected on attempt $i: Wayland"
return 0
elif [ -S "/tmp/.X11-unix/X0" ] 2>/dev/null; then
export DISPLAY=:0
log_message "Display server detected on attempt $i: X11"
return 0
fi
done
fi
fi
# Verify we have a display now
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
log_message "WARNING: No display server detected. This may cause graphics issues."
return 1
fi
log_message "Display environment ready: DISPLAY=$DISPLAY WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
return 0
}
# Load the user environment early
load_user_environment
# Function to check if player is healthy
check_health() {
# Check if heartbeat file exists and is recent (within last 60 seconds)

165
test_edited_media_upload.py Normal file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
Test script to diagnose edited media upload issues
Run this to test if the server endpoint exists and works correctly
"""
import json
import os
import sys
import requests
from pathlib import Path
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
def test_upload():
"""Test the edited media upload functionality"""
print("\n" + "="*60)
print("EDITED MEDIA UPLOAD DIAGNOSTICS")
print("="*60)
# Get authentication
try:
from get_playlists_v2 import get_auth_instance
auth = get_auth_instance()
if not auth or not auth.is_authenticated():
print("❌ ERROR: Not authenticated!")
print(" Please ensure player_auth.json exists and is valid")
return False
server_url = auth.auth_data.get('server_url')
auth_code = auth.auth_data.get('auth_code')
print(f"\n✓ Authentication successful")
print(f" Server URL: {server_url}")
print(f" Auth Code: {auth_code[:20]}..." if auth_code else " Auth Code: None")
except Exception as e:
print(f"❌ Authentication error: {e}")
return False
# Check for edited media files
edited_media_dir = os.path.join(os.path.dirname(__file__), 'media', 'edited_media')
edited_files = list(Path(edited_media_dir).glob('*_e_v*.jpg'))
metadata_files = list(Path(edited_media_dir).glob('*_metadata.json'))
print(f"\n✓ Edited Media Directory: {edited_media_dir}")
print(f" Edited images: {len(edited_files)}")
print(f" Metadata files: {len(metadata_files)}")
if not edited_files:
print("\n⚠️ No edited images found!")
print(" Create an edit first, then run this test")
return False
# Test with the first edited image
image_path = str(edited_files[0])
metadata_file = str(edited_files[0]).replace('.jpg', '_metadata.json')
if not os.path.exists(metadata_file):
print(f"\n❌ Metadata file not found: {metadata_file}")
return False
print(f"\nTesting upload with:")
print(f" Image: {os.path.basename(image_path)}")
print(f" Size: {os.path.getsize(image_path):,} bytes")
print(f" Metadata: {os.path.basename(metadata_file)}")
# Load and display metadata
with open(metadata_file, 'r') as f:
metadata = json.load(f)
print(f"\nMetadata content:")
for key, value in metadata.items():
print(f" {key}: {value}")
# Add original_filename if not present
metadata['original_filename'] = os.path.basename(metadata['original_path'])
# Prepare upload request
upload_url = f"{server_url}/api/player-edit-media"
headers = {'Authorization': f'Bearer {auth_code}'}
print(f"\n{'='*60}")
print("TESTING UPLOAD...")
print(f"{'='*60}")
print(f"Endpoint: {upload_url}")
print(f"Headers: Authorization: Bearer {auth_code[:20]}...")
try:
with open(image_path, 'rb') as img_file:
files = {
'image_file': (metadata['original_filename'], img_file, 'image/jpeg')
}
data = {
'metadata': json.dumps(metadata),
'original_file': metadata['original_filename']
}
print(f"\nSending request (30s timeout, SSL verify=False)...")
response = requests.post(
upload_url,
headers=headers,
files=files,
data=data,
timeout=30,
verify=False
)
print(f"\n✓ Response received!")
print(f" Status Code: {response.status_code}")
print(f" Headers: {dict(response.headers)}")
if response.status_code == 200:
print(f"\n✅ SUCCESS! Server accepted the upload")
print(f" Response: {response.json()}")
return True
elif response.status_code == 404:
print(f"\n❌ ENDPOINT NOT FOUND (404)")
print(f" The server does NOT have /api/player-edit-media endpoint")
print(f" Server may need to implement this feature")
elif response.status_code == 401:
print(f"\n❌ AUTHENTICATION FAILED (401)")
print(f" Check your auth_code in player_auth.json")
else:
print(f"\n❌ REQUEST FAILED (Status: {response.status_code})")
print(f" Response: {response.text}")
return False
except requests.exceptions.ConnectionError as e:
print(f"\n❌ CONNECTION ERROR")
print(f" Cannot reach server at {server_url}")
print(f" Error: {e}")
return False
except requests.exceptions.Timeout as e:
print(f"\n❌ TIMEOUT")
print(f" Server did not respond within 30 seconds")
print(f" Error: {e}")
return False
except requests.exceptions.SSLError as e:
print(f"\n❌ SSL ERROR")
print(f" Error: {e}")
print(f" Tip: Try adding verify=False to requests")
return False
except Exception as e:
print(f"\n❌ UNEXPECTED ERROR")
print(f" Error: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == '__main__':
success = test_upload()
print(f"\n{'='*60}")
if success:
print("✅ UPLOAD TEST PASSED - Server accepts edited media!")
else:
print("❌ UPLOAD TEST FAILED - See details above")
print(f"{'='*60}\n")
sys.exit(0 if success else 1)