Compare commits
10 Commits
6bf4e3735a
...
a8d6d70cd4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d6d70cd4 | ||
|
|
11436ddeab | ||
|
|
e2abde9f9c | ||
|
|
a825d299bf | ||
|
|
30f058182c | ||
|
|
9b58f6b63d | ||
|
|
eeb2a61ef7 | ||
|
|
120c889143 | ||
|
|
d1382af517 | ||
|
|
3531760e16 |
@@ -1,16 +1,61 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Aggressive display keep-alive for Raspberry Pi
|
# Aggressive display keep-alive for Raspberry Pi
|
||||||
# Prevents HDMI from powering down
|
# Supports both X11 and Wayland environments
|
||||||
|
|
||||||
DISPLAY_TIMEOUT=30
|
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
|
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
|
if command -v tvservice &> /dev/null; then
|
||||||
/usr/bin/tvservice -p 2>/dev/null
|
/usr/bin/tvservice -p 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Disable screensaver
|
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
|
if command -v xset &> /dev/null; then
|
||||||
DISPLAY=:0 xset s off 2>/dev/null
|
DISPLAY=:0 xset s off 2>/dev/null
|
||||||
DISPLAY=:0 xset -dpms 2>/dev/null
|
DISPLAY=:0 xset -dpms 2>/dev/null
|
||||||
@@ -28,6 +73,7 @@ while true; do
|
|||||||
if command -v xrandr &> /dev/null; then
|
if command -v xrandr &> /dev/null; then
|
||||||
DISPLAY=:0 xrandr --output HDMI-1 --power-profile performance 2>/dev/null || true
|
DISPLAY=:0 xrandr --output HDMI-1 --power-profile performance 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
sleep $DISPLAY_TIMEOUT
|
sleep $DISPLAY_TIMEOUT
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1768675283.8000998
|
1768682062.8464386
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Wait for desktop environment to be ready
|
# Wait for desktop environment to be ready
|
||||||
sleep 10
|
sleep 15
|
||||||
|
|
||||||
# Start the player
|
# Start the player
|
||||||
cd "/home/pi/Desktop/Kiwy-Signage" && bash start.sh
|
cd "/home/pi/Desktop/Kiwy-Signage" && bash start.sh
|
||||||
|
|||||||
54
.wait-for-display.sh
Executable file
54
.wait-for-display.sh
Executable 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
43
New.txt
Normal 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 $
|
||||||
|
|
||||||
70
install.sh
70
install.sh
@@ -35,29 +35,40 @@ setup_autostart() {
|
|||||||
SYSTEMD_DIR="$ACTUAL_HOME/.config/systemd/user"
|
SYSTEMD_DIR="$ACTUAL_HOME/.config/systemd/user"
|
||||||
LXDE_AUTOSTART="$ACTUAL_HOME/.config/lxsession/LXDE-pi/autostart"
|
LXDE_AUTOSTART="$ACTUAL_HOME/.config/lxsession/LXDE-pi/autostart"
|
||||||
|
|
||||||
# Method 1: XDG Autostart (works with most desktop environments)
|
# Method 1: XDG Autostart (Primary - works with Wayland/GNOME/KDE)
|
||||||
echo "Creating XDG autostart entry..."
|
echo "Creating XDG autostart entry for Wayland..."
|
||||||
mkdir -p "$AUTOSTART_DIR"
|
mkdir -p "$AUTOSTART_DIR"
|
||||||
|
|
||||||
|
# Create XDG desktop entry for autostart
|
||||||
cat > "$AUTOSTART_DIR/kivy-signage-player.desktop" << 'EOF'
|
cat > "$AUTOSTART_DIR/kivy-signage-player.desktop" << 'EOF'
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Kivy Signage Player
|
Name=Kivy Signage Player
|
||||||
Comment=Digital 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
|
Icon=media-video-display
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
NoDisplay=false
|
NoDisplay=false
|
||||||
Terminal=true
|
Terminal=false
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
Hidden=false
|
Hidden=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
X-GNOME-Autostart-delay=3
|
||||||
|
X-XFCE-Autostart-Override=true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Replace $SCRIPT_DIR with actual path in the file
|
# Replace $SCRIPT_DIR with actual path in the file
|
||||||
sed -i "s|\$SCRIPT_DIR|$SCRIPT_DIR|g" "$AUTOSTART_DIR/kivy-signage-player.desktop"
|
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"
|
chown "$ACTUAL_USER:$ACTUAL_USER" "$AUTOSTART_DIR/kivy-signage-player.desktop"
|
||||||
chmod +x "$AUTOSTART_DIR/kivy-signage-player.desktop"
|
chmod 644 "$AUTOSTART_DIR/kivy-signage-player.desktop"
|
||||||
echo "✓ XDG autostart entry created for user: $ACTUAL_USER"
|
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)
|
# Method 2: LXDE Autostart (for Raspberry Pi OS with LXDE)
|
||||||
if [ -f "$LXDE_AUTOSTART" ]; then
|
if [ -f "$LXDE_AUTOSTART" ]; then
|
||||||
@@ -71,37 +82,16 @@ EOF
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Method 3: systemd user service (more reliable)
|
# Method 3: Disable systemd service (prefer Wayland session management)
|
||||||
echo "Creating systemd user service..."
|
# XDG autostart above is sufficient for Wayland/GNOME sessions
|
||||||
mkdir -p "$SYSTEMD_DIR"
|
echo "Note: Using Wayland session autostart via XDG instead of systemd service"
|
||||||
|
|
||||||
cat > "$SYSTEMD_DIR/kivy-signage-player.service" << EOF
|
# If systemd service exists from previous installation, disable it
|
||||||
[Unit]
|
if sudo test -f /etc/systemd/system/kiwy-player.service 2>/dev/null; then
|
||||||
Description=Kivy Signage Player
|
echo "Disabling old systemd service in favor of Wayland session..."
|
||||||
After=graphical-session-started.target
|
sudo systemctl disable kiwy-player.service 2>/dev/null || true
|
||||||
PartOf=graphical-session.target
|
echo "✓ Old systemd service disabled"
|
||||||
|
fi
|
||||||
[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"
|
|
||||||
|
|
||||||
# Method 4: Cron job for fallback (starts at reboot)
|
# Method 4: Cron job for fallback (starts at reboot)
|
||||||
echo "Setting up cron fallback..."
|
echo "Setting up cron fallback..."
|
||||||
@@ -110,7 +100,7 @@ EOF
|
|||||||
cat > "$CRON_WRAPPER" << 'EOF'
|
cat > "$CRON_WRAPPER" << 'EOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Wait for desktop environment to be ready
|
# Wait for desktop environment to be ready
|
||||||
sleep 10
|
sleep 15
|
||||||
|
|
||||||
# Start the player
|
# Start the player
|
||||||
cd "$SCRIPT_DIR" && bash start.sh
|
cd "$SCRIPT_DIR" && bash start.sh
|
||||||
@@ -119,8 +109,9 @@ EOF
|
|||||||
chmod +x "$CRON_WRAPPER"
|
chmod +x "$CRON_WRAPPER"
|
||||||
|
|
||||||
# Add to crontab for the actual user
|
# 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
|
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"
|
echo "✓ Cron fallback configured"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -133,6 +124,9 @@ EOF
|
|||||||
fi
|
fi
|
||||||
echo " 4. ✓ Cron Fallback (@reboot)"
|
echo " 4. ✓ Cron Fallback (@reboot)"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "Status check command:"
|
||||||
|
echo " systemctl --user status kivy-signage-player.service"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to disable power-saving mode on Raspberry Pi
|
# Function to disable power-saving mode on Raspberry Pi
|
||||||
|
|||||||
@@ -23,5 +23,5 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"playlist_id": 1,
|
"playlist_id": 1,
|
||||||
"playlist_version": 33
|
"playlist_version": 34
|
||||||
}
|
}
|
||||||
@@ -336,15 +336,18 @@ class EditPopup(Popup):
|
|||||||
args=(output_path, json_filename),
|
args=(output_path, json_filename),
|
||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
upload_thread.start()
|
upload_thread.start() # Re-enabled
|
||||||
Logger.info(f"EditPopup: Background upload thread started")
|
|
||||||
|
|
||||||
# NOW show saving popup AFTER everything is done
|
# NOW show saving popup AFTER everything is done
|
||||||
def show_saving_and_dismiss(dt):
|
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(
|
save_label = Label(
|
||||||
text='Saved! Reloading player...',
|
text=save_msg,
|
||||||
font_size='36sp',
|
font_size='24sp',
|
||||||
color=(1, 1, 1, 1),
|
color=(1, 1, 1, 1),
|
||||||
bold=True
|
bold=True
|
||||||
)
|
)
|
||||||
@@ -352,7 +355,7 @@ class EditPopup(Popup):
|
|||||||
saving_popup = Popup(
|
saving_popup = Popup(
|
||||||
title='',
|
title='',
|
||||||
content=save_label,
|
content=save_label,
|
||||||
size_hint=(0.8, 0.3),
|
size_hint=(0.85, 0.4),
|
||||||
auto_dismiss=False,
|
auto_dismiss=False,
|
||||||
separator_height=0,
|
separator_height=0,
|
||||||
background_color=(0.2, 0.7, 0.2, 0.95) # Green background
|
background_color=(0.2, 0.7, 0.2, 0.95) # Green background
|
||||||
@@ -360,13 +363,23 @@ class EditPopup(Popup):
|
|||||||
saving_popup.open()
|
saving_popup.open()
|
||||||
Logger.info("EditPopup: Saving confirmation popup opened")
|
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):
|
def dismiss_all(dt):
|
||||||
saving_popup.dismiss()
|
saving_popup.dismiss()
|
||||||
Logger.info(f"EditPopup: Dismissing to resume playback...")
|
Logger.info(f"EditPopup: Dismissing to resume playback...")
|
||||||
self.dismiss()
|
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
|
# Small delay to ensure UI is ready, then show popup
|
||||||
Clock.schedule_once(show_saving_and_dismiss, 0.1)
|
Clock.schedule_once(show_saving_and_dismiss, 0.1)
|
||||||
@@ -420,7 +433,8 @@ class EditPopup(Popup):
|
|||||||
# Get authenticated instance
|
# Get authenticated instance
|
||||||
auth = get_auth_instance()
|
auth = get_auth_instance()
|
||||||
if not auth or not auth.is_authenticated():
|
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
|
return False
|
||||||
|
|
||||||
server_url = auth.auth_data.get('server_url')
|
server_url = auth.auth_data.get('server_url')
|
||||||
@@ -434,60 +448,93 @@ class EditPopup(Popup):
|
|||||||
with open(metadata_path, 'r') as meta_file:
|
with open(metadata_path, 'r') as meta_file:
|
||||||
metadata = json.load(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"
|
upload_url = f"{server_url}/api/player-edit-media"
|
||||||
headers = {'Authorization': f'Bearer {auth_code}'}
|
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
|
# Prepare file and data for upload
|
||||||
with open(image_path, 'rb') as img_file:
|
with open(image_path, 'rb') as img_file:
|
||||||
files = {
|
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
|
# Send metadata as JSON string in form data
|
||||||
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}")
|
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: - Original file: {metadata['original_filename']}")
|
||||||
|
Logger.info(f"EditPopup: - Edited image: {image_path}")
|
||||||
|
Logger.info(f"EditPopup: - Metadata: {metadata_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(upload_url, headers=headers, files=files, data=data, timeout=30, verify=False)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
Logger.info(f"EditPopup: ✅ Successfully uploaded edited media to server: {response_data}")
|
Logger.info(f"EditPopup: ✅ Successfully uploaded edited media to server")
|
||||||
|
Logger.info(f"EditPopup: Server response: {response_data}")
|
||||||
|
|
||||||
# Delete local files after successful upload
|
# 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}")
|
||||||
|
|
||||||
|
# Trigger playlist reload if server provides new version
|
||||||
try:
|
try:
|
||||||
if os.path.exists(image_path):
|
new_version = response_data.get('new_playlist_version')
|
||||||
os.remove(image_path)
|
if new_version:
|
||||||
Logger.info(f"EditPopup: Deleted local image file: {os.path.basename(image_path)}")
|
Logger.info(f"EditPopup: 📡 Server reports new playlist version: {new_version}")
|
||||||
|
Logger.info(f"EditPopup: Triggering playlist reload on next cycle...")
|
||||||
|
|
||||||
if os.path.exists(metadata_path):
|
# Playlist reload disabled for now - was causing crashes
|
||||||
os.remove(metadata_path)
|
# Will be re-enabled with better implementation
|
||||||
Logger.info(f"EditPopup: Deleted local metadata file: {os.path.basename(metadata_path)}")
|
Logger.info(f"EditPopup: ✓ Edited media uploaded successfully")
|
||||||
|
|
||||||
Logger.info("EditPopup: ✅ Local edited files cleaned up after successful upload")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.warning(f"EditPopup: Could not delete local files: {e}")
|
Logger.warning(f"EditPopup: Could not process playlist version from server: {e}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
elif response.status_code == 404:
|
elif response.status_code == 404:
|
||||||
Logger.warning("EditPopup: ⚠️ Upload endpoint not available on server (404) - edited media saved locally only")
|
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
|
return False
|
||||||
else:
|
else:
|
||||||
Logger.warning(f"EditPopup: ⚠️ Upload failed with status {response.status_code} - edited media saved locally only")
|
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
|
return False
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
Logger.warning("EditPopup: ⚠️ Upload timed out - edited media saved locally only")
|
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
|
return False
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError as e:
|
||||||
Logger.warning("EditPopup: ⚠️ Cannot connect to server - edited media saved locally only")
|
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
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
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
|
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
|
return False
|
||||||
|
|
||||||
def close_without_saving(self, instance):
|
def close_without_saving(self, instance):
|
||||||
|
|||||||
76
src/main.py
76
src/main.py
@@ -15,13 +15,17 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
|
os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
|
||||||
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs
|
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs
|
||||||
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' # Prevent screen saver
|
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
|
# 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_AUDIO'] = 'ffpyplayer' # Use ffpyplayer for audio
|
||||||
os.environ['KIVY_GL_BACKEND'] = 'gl' # Use OpenGL backend
|
os.environ['KIVY_GL_BACKEND'] = 'gl' # Use OpenGL backend
|
||||||
os.environ['FFMPEG_THREADS'] = '4' # Use 4 threads for ffmpeg decoding
|
os.environ['KIVY_INPUTPROVIDERS'] = 'wayland,x11' # Only use Wayland and X11 input providers, skip problematic ones
|
||||||
os.environ['LIBPLAYER_BUFFER'] = '2048000' # 2MB buffer for smooth playback
|
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
|
os.environ['SDL_AUDIODRIVER'] = 'alsa' # Use ALSA for better audio on Pi
|
||||||
|
|
||||||
# Configure Kivy BEFORE importing any Kivy modules
|
# Configure Kivy BEFORE importing any Kivy modules
|
||||||
@@ -35,7 +39,7 @@ Config.set('graphics', 'window_state', 'maximized') # Maximize window
|
|||||||
# Video and audio performance settings
|
# Video and audio performance settings
|
||||||
Config.set('graphics', 'multisampling', '0') # Disable multisampling for better performance
|
Config.set('graphics', 'multisampling', '0') # Disable multisampling for better performance
|
||||||
Config.set('graphics', 'fast_rgba', '1') # Enable fast RGBA 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
|
Config.set('kivy', 'log_level', 'warning') # Reduce logging overhead
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
@@ -100,8 +104,22 @@ class CardReader:
|
|||||||
Logger.error("CardReader: evdev library not available")
|
Logger.error("CardReader: evdev library not available")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
try:
|
try:
|
||||||
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
|
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
|
# Log all available devices for debugging
|
||||||
Logger.info("CardReader: Scanning input devices...")
|
Logger.info("CardReader: Scanning input devices...")
|
||||||
@@ -486,8 +504,12 @@ class CustomVKeyboard(VKeyboard):
|
|||||||
|
|
||||||
Logger.info("CustomVKeyboard: Wrapped in container with close button")
|
Logger.info("CustomVKeyboard: Wrapped in container with close button")
|
||||||
|
|
||||||
# Set the custom keyboard factory
|
# Set the custom keyboard factory (only if Window is properly initialized)
|
||||||
|
if Window is not None:
|
||||||
|
try:
|
||||||
Window.set_vkeyboard_class(CustomVKeyboard)
|
Window.set_vkeyboard_class(CustomVKeyboard)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"CustomVKeyboard: Could not set custom keyboard: {e}")
|
||||||
|
|
||||||
class ExitPasswordPopup(Popup):
|
class ExitPasswordPopup(Popup):
|
||||||
def __init__(self, player_instance, was_paused=False, **kwargs):
|
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.auto_resume_event = None # Track scheduled auto-resume
|
||||||
self.config = {}
|
self.config = {}
|
||||||
self.playlist_version = None
|
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.consecutive_errors = 0 # Track consecutive playback errors
|
||||||
self.max_consecutive_errors = 10 # Maximum errors before stopping
|
self.max_consecutive_errors = 10 # Maximum errors before stopping
|
||||||
self.intro_played = False # Track if intro has been played
|
self.intro_played = False # Track if intro has been played
|
||||||
@@ -958,7 +981,7 @@ class SignagePlayer(Widget):
|
|||||||
# Wayland-specific commands
|
# Wayland-specific commands
|
||||||
|
|
||||||
# Method 1: Use wlopm (Wayland output power management)
|
# 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
|
# Method 2: Use wlr-randr for wlroots compositors
|
||||||
os.system('wlr-randr --output HDMI-A-1 --on 2>/dev/null || true')
|
os.system('wlr-randr --output HDMI-A-1 --on 2>/dev/null || true')
|
||||||
@@ -1023,6 +1046,7 @@ class SignagePlayer(Widget):
|
|||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
"""Load configuration from file"""
|
"""Load configuration from file"""
|
||||||
|
Logger.debug("SignagePlayer: load_config() starting...")
|
||||||
try:
|
try:
|
||||||
if os.path.exists(self.config_file):
|
if os.path.exists(self.config_file):
|
||||||
with open(self.config_file, 'r') as f:
|
with open(self.config_file, 'r') as f:
|
||||||
@@ -1156,13 +1180,14 @@ class SignagePlayer(Widget):
|
|||||||
|
|
||||||
def play_intro_video(self):
|
def play_intro_video(self):
|
||||||
"""Play intro video on startup"""
|
"""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')
|
intro_path = os.path.join(self.resources_path, 'intro1.mp4')
|
||||||
|
|
||||||
if not os.path.exists(intro_path):
|
if not os.path.exists(intro_path):
|
||||||
Logger.warning(f"SignagePlayer: Intro video not found at {intro_path}")
|
Logger.warning(f"SignagePlayer: Intro video not found at {intro_path}")
|
||||||
# Skip intro and load playlist
|
# Skip intro and load playlist
|
||||||
self.intro_played = True
|
self.intro_played = True
|
||||||
self.check_playlist_and_play(None)
|
Clock.schedule_once(self.check_playlist_and_play, 0.1)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1180,18 +1205,34 @@ class SignagePlayer(Widget):
|
|||||||
|
|
||||||
# Bind to video end event
|
# Bind to video end event
|
||||||
def on_intro_end(instance, value):
|
def on_intro_end(instance, value):
|
||||||
|
try:
|
||||||
if value == 'stop':
|
if value == 'stop':
|
||||||
Logger.info("SignagePlayer: Intro video finished")
|
Logger.info("SignagePlayer: Intro video finished")
|
||||||
|
|
||||||
# Mark intro as played before removing video
|
# Mark intro as played before removing video
|
||||||
self.intro_played = True
|
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
|
# Remove intro video
|
||||||
|
try:
|
||||||
if intro_video in self.ids.content_area.children:
|
if intro_video in self.ids.content_area.children:
|
||||||
self.ids.content_area.remove_widget(intro_video)
|
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
|
# Start normal playlist immediately to reduce white screen
|
||||||
|
Logger.debug("SignagePlayer: Triggering playlist check after intro")
|
||||||
self.check_playlist_and_play(None)
|
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)
|
intro_video.bind(state=on_intro_end)
|
||||||
|
|
||||||
@@ -1200,9 +1241,11 @@ class SignagePlayer(Widget):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.error(f"SignagePlayer: Error playing intro video: {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
|
# Skip intro and load playlist
|
||||||
self.intro_played = True
|
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):
|
def check_playlist_and_play(self, dt):
|
||||||
"""Check for playlist updates and ensure playback is running"""
|
"""Check for playlist updates and ensure playback is running"""
|
||||||
@@ -1837,18 +1880,22 @@ class SignagePlayerApp(App):
|
|||||||
else:
|
else:
|
||||||
Logger.info(f"SignagePlayerApp: Using auto resolution (no constraint)")
|
Logger.info(f"SignagePlayerApp: Using auto resolution (no constraint)")
|
||||||
|
|
||||||
# Force fullscreen and borderless
|
# Force fullscreen and borderless (only if Window is available)
|
||||||
|
if Window is not None:
|
||||||
Window.fullscreen = True
|
Window.fullscreen = True
|
||||||
Window.borderless = True
|
Window.borderless = True
|
||||||
Logger.info(f"SignagePlayerApp: Screen size: {Window.size}")
|
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'}")
|
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
|
# Hide cursor after 3 seconds of inactivity
|
||||||
Clock.schedule_once(self.hide_cursor, 3)
|
Clock.schedule_once(self.hide_cursor, 3)
|
||||||
|
else:
|
||||||
|
Logger.critical("SignagePlayerApp: Window is None - display server not available")
|
||||||
return SignagePlayer()
|
return SignagePlayer()
|
||||||
|
|
||||||
def hide_cursor(self, dt):
|
def hide_cursor(self, dt):
|
||||||
"""Hide the mouse cursor"""
|
"""Hide the mouse cursor"""
|
||||||
try:
|
try:
|
||||||
|
if Window is not None:
|
||||||
Window.show_cursor = False
|
Window.show_cursor = False
|
||||||
except:
|
except:
|
||||||
pass # Some platforms don't support cursor hiding
|
pass # Some platforms don't support cursor hiding
|
||||||
@@ -1856,13 +1903,16 @@ class SignagePlayerApp(App):
|
|||||||
def on_start(self):
|
def on_start(self):
|
||||||
# Setup asyncio event loop for Kivy integration
|
# Setup asyncio event loop for Kivy integration
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
# Use get_running_loop in Python 3.7+ to avoid deprecation warning
|
||||||
Logger.info("SignagePlayerApp: Asyncio event loop integrated with Kivy")
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
# Create new event loop if none exists
|
# No running loop, create a new one
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
Logger.info("SignagePlayerApp: New asyncio event loop created")
|
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
|
# Schedule periodic async task processing
|
||||||
Clock.schedule_interval(self._process_async_tasks, 0.1) # Process every 100ms
|
Clock.schedule_interval(self._process_async_tasks, 0.1) # Process every 100ms
|
||||||
|
|||||||
84
start.sh
84
start.sh
@@ -14,11 +14,91 @@ HEARTBEAT_FILE="$SCRIPT_DIR/.player_heartbeat"
|
|||||||
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
|
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
|
||||||
LOG_FILE="$SCRIPT_DIR/player_watchdog.log"
|
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() {
|
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
|
# Function to check if player is healthy
|
||||||
check_health() {
|
check_health() {
|
||||||
# Check if heartbeat file exists and is recent (within last 60 seconds)
|
# Check if heartbeat file exists and is recent (within last 60 seconds)
|
||||||
|
|||||||
165
test_edited_media_upload.py
Normal file
165
test_edited_media_upload.py
Normal 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)
|
||||||
Reference in New Issue
Block a user