Improve video playback performance with async server communication
Changes: - Implement async/await pattern for non-blocking server communications - Use asyncio event loop integrated with Kivy for smooth UI - Run playlist updates and feedback sending in thread pool - Prevent UI freezing during network I/O operations - Add video error handling and playback state management - Implement delayed video playback start (wait 0.5s for loading) - Add video error callback for better error recovery - Cancel all async tasks on application shutdown - Process async tasks every 100ms without blocking Kivy event loop New file: - convert_video_for_rpi.sh: FFmpeg script for optimizing videos (1080p @ 30fps, H.264)
This commit is contained in:
57
convert_video_for_rpi.sh
Executable file
57
convert_video_for_rpi.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Video Conversion Script for Raspberry Pi Signage Player
|
||||||
|
# Converts videos to optimal settings: 1080p @ 30fps, H.264 codec
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: $0 <input_video> [output_video]"
|
||||||
|
echo "Example: $0 input.mp4 output.mp4"
|
||||||
|
echo ""
|
||||||
|
echo "This script converts videos to Raspberry Pi-friendly settings:"
|
||||||
|
echo " - Resolution: Max 1920x1080"
|
||||||
|
echo " - Frame rate: 30 fps"
|
||||||
|
echo " - Codec: H.264"
|
||||||
|
echo " - Bitrate: ~5-8 Mbps"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INPUT_VIDEO="$1"
|
||||||
|
OUTPUT_VIDEO="${2:-converted_$(basename "$INPUT_VIDEO")}"
|
||||||
|
|
||||||
|
if [ ! -f "$INPUT_VIDEO" ]; then
|
||||||
|
echo "Error: Input file '$INPUT_VIDEO' not found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Converting video for Raspberry Pi playback..."
|
||||||
|
echo "Input: $INPUT_VIDEO"
|
||||||
|
echo "Output: $OUTPUT_VIDEO"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Convert video with optimal settings for Raspberry Pi
|
||||||
|
ffmpeg -i "$INPUT_VIDEO" \
|
||||||
|
-c:v libx264 \
|
||||||
|
-preset medium \
|
||||||
|
-crf 23 \
|
||||||
|
-maxrate 8M \
|
||||||
|
-bufsize 12M \
|
||||||
|
-vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,fps=30" \
|
||||||
|
-r 30 \
|
||||||
|
-c:a aac \
|
||||||
|
-b:a 128k \
|
||||||
|
-movflags +faststart \
|
||||||
|
-y \
|
||||||
|
"$OUTPUT_VIDEO"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✓ Conversion completed successfully!"
|
||||||
|
echo "Original: $(du -h "$INPUT_VIDEO" | cut -f1)"
|
||||||
|
echo "Converted: $(du -h "$OUTPUT_VIDEO" | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
echo "You can now use '$OUTPUT_VIDEO' in your signage player."
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "✗ Conversion failed! Make sure ffmpeg is installed:"
|
||||||
|
echo " sudo apt-get install ffmpeg"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
153
src/main.py
153
src/main.py
@@ -8,6 +8,8 @@ import json
|
|||||||
import platform
|
import platform
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import asyncio
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
# Set environment variables for better video performance
|
# Set environment variables for better video performance
|
||||||
os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
|
os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
|
||||||
@@ -227,8 +229,9 @@ class SignagePlayer(Widget):
|
|||||||
# Load configuration
|
# Load configuration
|
||||||
self.load_config()
|
self.load_config()
|
||||||
|
|
||||||
# Start playlist update thread
|
# Start async server tasks (non-blocking)
|
||||||
threading.Thread(target=self.playlist_update_loop, daemon=True).start()
|
asyncio.ensure_future(self.async_playlist_update_loop())
|
||||||
|
Logger.info("SignagePlayer: Async server tasks started")
|
||||||
|
|
||||||
# Start media playback
|
# Start media playback
|
||||||
Clock.schedule_interval(self.check_playlist_and_play, 30) # Check every 30 seconds
|
Clock.schedule_interval(self.check_playlist_and_play, 30) # Check every 30 seconds
|
||||||
@@ -267,16 +270,26 @@ class SignagePlayer(Widget):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.error(f"SignagePlayer: Error saving config: {e}")
|
Logger.error(f"SignagePlayer: Error saving config: {e}")
|
||||||
|
|
||||||
def playlist_update_loop(self):
|
async def async_playlist_update_loop(self):
|
||||||
"""Background thread to check for playlist updates"""
|
"""Async coroutine to check for playlist updates without blocking UI"""
|
||||||
|
Logger.info("SignagePlayer: Starting async playlist update loop")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if self.config:
|
if self.config:
|
||||||
# Find latest playlist file
|
# Run blocking network I/O in thread pool
|
||||||
latest_playlist = self.get_latest_playlist_file()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
# Check for updates
|
# Find latest playlist file
|
||||||
updated = update_playlist_if_needed(
|
latest_playlist = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
self.get_latest_playlist_file
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for updates (network I/O in thread pool)
|
||||||
|
updated = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
update_playlist_if_needed,
|
||||||
latest_playlist,
|
latest_playlist,
|
||||||
self.config,
|
self.config,
|
||||||
self.media_dir,
|
self.media_dir,
|
||||||
@@ -285,13 +298,24 @@ class SignagePlayer(Widget):
|
|||||||
|
|
||||||
if updated:
|
if updated:
|
||||||
Logger.info("SignagePlayer: Playlist updated, reloading...")
|
Logger.info("SignagePlayer: Playlist updated, reloading...")
|
||||||
|
# Schedule UI update on main thread
|
||||||
Clock.schedule_once(self.load_playlist, 0)
|
Clock.schedule_once(self.load_playlist, 0)
|
||||||
|
|
||||||
time.sleep(60) # Check every minute
|
# Wait 60 seconds before next check
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.error(f"SignagePlayer: Error in playlist update loop: {e}")
|
Logger.error(f"SignagePlayer: Error in async playlist update loop: {e}")
|
||||||
time.sleep(30) # Wait 30 seconds on error
|
await asyncio.sleep(30) # Wait 30 seconds on error
|
||||||
|
|
||||||
|
async def async_send_feedback(self, feedback_func, *args):
|
||||||
|
"""Send feedback to server asynchronously without blocking UI"""
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
# Run feedback in thread pool to avoid blocking
|
||||||
|
await loop.run_in_executor(None, feedback_func, *args)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.debug(f"SignagePlayer: Error sending async feedback: {e}")
|
||||||
|
|
||||||
def get_latest_playlist_file(self):
|
def get_latest_playlist_file(self):
|
||||||
"""Get the path to the latest playlist file"""
|
"""Get the path to the latest playlist file"""
|
||||||
@@ -403,9 +427,16 @@ class SignagePlayer(Widget):
|
|||||||
self.next_media()
|
self.next_media()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Send feedback to server
|
# Send feedback to server asynchronously (non-blocking)
|
||||||
if self.config:
|
if self.config:
|
||||||
send_playing_status_feedback(self.config, self.playlist_version, file_name)
|
asyncio.ensure_future(
|
||||||
|
self.async_send_feedback(
|
||||||
|
send_playing_status_feedback,
|
||||||
|
self.config,
|
||||||
|
self.playlist_version,
|
||||||
|
file_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Reset error counter on successful playback
|
# Reset error counter on successful playback
|
||||||
self.consecutive_errors = 0
|
self.consecutive_errors = 0
|
||||||
@@ -428,15 +459,18 @@ class SignagePlayer(Widget):
|
|||||||
def play_video(self, video_path, duration):
|
def play_video(self, video_path, duration):
|
||||||
"""Play a video file using Kivy's Video widget"""
|
"""Play a video file using Kivy's Video widget"""
|
||||||
try:
|
try:
|
||||||
# Create Video widget with optimized settings for quality
|
# Verify file exists
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
Logger.error(f"Video file not found: {video_path}")
|
||||||
|
self.next_media()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create Video widget with optimized settings
|
||||||
self.current_widget = Video(
|
self.current_widget = Video(
|
||||||
source=video_path,
|
source=video_path,
|
||||||
state='play',
|
state='stop', # Start stopped, then play after loaded
|
||||||
options={
|
options={
|
||||||
'eos': 'stop', # Stop at end of stream
|
'eos': 'stop', # Stop at end of stream
|
||||||
'autoplay': True, # Auto-play when loaded
|
|
||||||
'allow_stretch': True, # Allow stretching to fit
|
|
||||||
'sync': 'audio', # Sync video to audio for smooth playback
|
|
||||||
},
|
},
|
||||||
allow_stretch=True,
|
allow_stretch=True,
|
||||||
keep_ratio=True, # Maintain aspect ratio
|
keep_ratio=True, # Maintain aspect ratio
|
||||||
@@ -444,15 +478,15 @@ class SignagePlayer(Widget):
|
|||||||
pos_hint={'center_x': 0.5, 'center_y': 0.5}
|
pos_hint={'center_x': 0.5, 'center_y': 0.5}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bind to loaded event to log video info
|
# Bind to loaded and error events
|
||||||
self.current_widget.bind(loaded=self._on_video_loaded)
|
self.current_widget.bind(loaded=self._on_video_loaded)
|
||||||
|
|
||||||
# Add to content area
|
# Add to content area
|
||||||
self.ids.content_area.add_widget(self.current_widget)
|
self.ids.content_area.add_widget(self.current_widget)
|
||||||
Logger.info(f"SignagePlayer: Playing {os.path.basename(video_path)} for {duration}s")
|
Logger.info(f"SignagePlayer: Playing {os.path.basename(video_path)} for {duration}s")
|
||||||
|
|
||||||
# Schedule next media after duration
|
# Wait a bit for video to load, then start playing
|
||||||
Clock.schedule_once(self.next_media, duration)
|
Clock.schedule_once(lambda dt: self._start_video_playback(duration), 0.5)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.error(f"SignagePlayer: Error playing video {video_path}: {e}")
|
Logger.error(f"SignagePlayer: Error playing video {video_path}: {e}")
|
||||||
@@ -460,6 +494,29 @@ class SignagePlayer(Widget):
|
|||||||
if self.consecutive_errors < self.max_consecutive_errors:
|
if self.consecutive_errors < self.max_consecutive_errors:
|
||||||
self.next_media()
|
self.next_media()
|
||||||
|
|
||||||
|
def _start_video_playback(self, duration):
|
||||||
|
"""Start video playback after widget is loaded"""
|
||||||
|
try:
|
||||||
|
if self.current_widget and hasattr(self.current_widget, 'state'):
|
||||||
|
self.current_widget.state = 'play'
|
||||||
|
Logger.info("SignagePlayer: Video playback started")
|
||||||
|
# Schedule next media after duration
|
||||||
|
Clock.schedule_once(self.next_media, duration)
|
||||||
|
else:
|
||||||
|
Logger.error("SignagePlayer: Video widget not ready")
|
||||||
|
self.next_media()
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"SignagePlayer: Error starting video playback: {e}")
|
||||||
|
self.next_media()
|
||||||
|
|
||||||
|
def _on_video_error(self, instance, error):
|
||||||
|
"""Callback when video encounters an error"""
|
||||||
|
Logger.error(f"Video playback error: {error}")
|
||||||
|
Logger.error(f"Video source: {instance.source}")
|
||||||
|
Logger.error(f"Video state: {instance.state}")
|
||||||
|
# Try to skip to next media on error
|
||||||
|
Clock.schedule_once(self.next_media, 1)
|
||||||
|
|
||||||
def _on_video_loaded(self, instance, value):
|
def _on_video_loaded(self, instance, value):
|
||||||
"""Callback when video is loaded - log video information"""
|
"""Callback when video is loaded - log video information"""
|
||||||
if value:
|
if value:
|
||||||
@@ -524,9 +581,15 @@ class SignagePlayer(Widget):
|
|||||||
"""Restart playlist from beginning"""
|
"""Restart playlist from beginning"""
|
||||||
Logger.info("SignagePlayer: Restarting playlist")
|
Logger.info("SignagePlayer: Restarting playlist")
|
||||||
|
|
||||||
# Send restart feedback
|
# Send restart feedback asynchronously (non-blocking)
|
||||||
if self.config:
|
if self.config:
|
||||||
send_playlist_restart_feedback(self.config, self.playlist_version)
|
asyncio.ensure_future(
|
||||||
|
self.async_send_feedback(
|
||||||
|
send_playlist_restart_feedback,
|
||||||
|
self.config,
|
||||||
|
self.playlist_version
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.current_index = 0
|
self.current_index = 0
|
||||||
self.play_current_media()
|
self.play_current_media()
|
||||||
@@ -535,9 +598,16 @@ class SignagePlayer(Widget):
|
|||||||
"""Show error message"""
|
"""Show error message"""
|
||||||
Logger.error(f"SignagePlayer: {message}")
|
Logger.error(f"SignagePlayer: {message}")
|
||||||
|
|
||||||
# Send error feedback to server
|
# Send error feedback to server asynchronously (non-blocking)
|
||||||
if self.config:
|
if self.config:
|
||||||
send_player_error_feedback(self.config, message, self.playlist_version)
|
asyncio.ensure_future(
|
||||||
|
self.async_send_feedback(
|
||||||
|
send_player_error_feedback,
|
||||||
|
self.config,
|
||||||
|
message,
|
||||||
|
self.playlist_version
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Show error on screen
|
# Show error on screen
|
||||||
self.ids.status_label.text = f"Error: {message}"
|
self.ids.status_label.text = f"Error: {message}"
|
||||||
@@ -710,14 +780,47 @@ class SignagePlayerApp(App):
|
|||||||
pass # Some platforms don't support cursor hiding
|
pass # Some platforms don't support cursor hiding
|
||||||
|
|
||||||
def on_start(self):
|
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")
|
||||||
|
|
||||||
|
# Schedule periodic async task processing
|
||||||
|
Clock.schedule_interval(self._process_async_tasks, 0.1) # Process every 100ms
|
||||||
|
|
||||||
# Log final window info
|
# Log final window info
|
||||||
Logger.info(f"SignagePlayerApp: Final window size: {Window.size}")
|
Logger.info(f"SignagePlayerApp: Final window size: {Window.size}")
|
||||||
Logger.info(f"SignagePlayerApp: Fullscreen: {Window.fullscreen}")
|
Logger.info(f"SignagePlayerApp: Fullscreen: {Window.fullscreen}")
|
||||||
Logger.info("SignagePlayerApp: Application started")
|
Logger.info("SignagePlayerApp: Application started")
|
||||||
|
Logger.info("SignagePlayerApp: Server communications running asynchronously")
|
||||||
|
|
||||||
|
def _process_async_tasks(self, dt):
|
||||||
|
"""Process pending asyncio tasks without blocking Kivy"""
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
# Process pending callbacks without blocking
|
||||||
|
loop.call_soon(loop.stop)
|
||||||
|
loop.run_forever()
|
||||||
|
except Exception as e:
|
||||||
|
pass # Silently handle - loop may not have tasks
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
Logger.info("SignagePlayerApp: Application stopped")
|
Logger.info("SignagePlayerApp: Application stopped")
|
||||||
|
|
||||||
|
# Cancel all async tasks
|
||||||
|
try:
|
||||||
|
pending = asyncio.all_tasks()
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
Logger.info(f"SignagePlayerApp: Cancelled {len(pending)} async tasks")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.debug(f"SignagePlayerApp: Error cancelling tasks: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
SignagePlayerApp().run()
|
SignagePlayerApp().run()
|
||||||
Reference in New Issue
Block a user