| @@ -0,0 +1,35 @@ | |||||
| # Copyright 2015 Adafruit Industries. | |||||
| # Author: Tony DiCola | |||||
| # License: GNU GPLv2, see LICENSE.txt | |||||
| class DirectoryReader(object): | |||||
| def __init__(self, config): | |||||
| """Create an instance of a file reader that just reads a single | |||||
| directory on disk. | |||||
| """ | |||||
| self._load_config(config) | |||||
| def _load_config(self, config): | |||||
| self._path = config.get('directory', 'path') | |||||
| def search_paths(self): | |||||
| """Return a list of paths to search for files.""" | |||||
| return [self._path] | |||||
| def is_changed(self): | |||||
| """Return true if the file search paths have changed.""" | |||||
| # For now just return false and assume the path never changes. In the | |||||
| # future it might be interesting to watch for file changes and return | |||||
| # true if new files are added/removed from the directory. This is | |||||
| # called in a tight loop of the main program so it needs to be fast and | |||||
| # not resource intensive. | |||||
| return False | |||||
| def idle_message(self): | |||||
| """Return a message to display when idle and no files are found.""" | |||||
| return 'No files found in {0}'.format(self._path) | |||||
| def create_file_reader(config): | |||||
| """Create new file reader based on reading a directory on disk.""" | |||||
| return DirectoryReader(config) | |||||
| @@ -0,0 +1,69 @@ | |||||
| # Copyright 2015 Adafruit Industries. | |||||
| # Author: Tony DiCola | |||||
| # License: GNU GPLv2, see LICENSE.txt | |||||
| import os | |||||
| import subprocess | |||||
| import time | |||||
| class HelloVideoPlayer(object): | |||||
| def __init__(self, config): | |||||
| """Create an instance of a video player that runs hello_video.bin in the | |||||
| background. | |||||
| """ | |||||
| self._process = None | |||||
| self._load_config(config) | |||||
| def _load_config(self, config): | |||||
| self._extensions = config.get('hello_video', 'extensions') \ | |||||
| .translate(None, ' \t\r\n.') \ | |||||
| .split(',') | |||||
| def supported_extensions(self): | |||||
| """Return list of supported file extensions.""" | |||||
| return self._extensions | |||||
| def play(self, movie, loop=False): | |||||
| """Play the provided movied file, optionally looping it repeatedly.""" | |||||
| self.stop(3) # Up to 3 second delay to let the old player stop. | |||||
| # Assemble list of arguments. | |||||
| args = ['hello_video.bin'] | |||||
| if loop: | |||||
| args.append('--loop') # Add loop parameter if necessary. | |||||
| args.append(movie) # Add movie file path. | |||||
| # Run hello_video process and direct standard output to /dev/null. | |||||
| self._process = subprocess.Popen(args, | |||||
| stdout=open(os.devnull, 'wb'), | |||||
| close_fds=True) | |||||
| def is_playing(self): | |||||
| """Return true if the video player is running, false otherwise.""" | |||||
| if self._process is None: | |||||
| return False | |||||
| self._process.poll() | |||||
| return self._process.returncode is None | |||||
| def stop(self, block_timeout_sec=None): | |||||
| """Stop the video player. block_timeout_sec is how many seconds to | |||||
| block waiting for the player to stop before moving on. | |||||
| """ | |||||
| # Stop the player if it's running. | |||||
| if self._process is not None and self._process.returncode is None: | |||||
| # process.kill() doesn't seem to work reliably if USB drive is | |||||
| # removed, instead just run a kill -9 on it. | |||||
| subprocess.call(['kill', '-9', str(self._process.pid)]) | |||||
| # If a blocking timeout was specified, wait up to that amount of time | |||||
| # for the process to stop. | |||||
| start = time.time() | |||||
| while self._process is not None and self._process.returncode is None: | |||||
| if (time.time() - start) >= block_timeout_sec: | |||||
| break | |||||
| time.sleep(0) | |||||
| # Let the process be garbage collected. | |||||
| self._process = None | |||||
| def create_player(config): | |||||
| """Create new video player based on hello_video.""" | |||||
| return HelloVideoPlayer(config) | |||||
| @@ -0,0 +1,31 @@ | |||||
| # Copyright 2015 Adafruit Industries. | |||||
| # Author: Tony DiCola | |||||
| # License: GNU GPLv2, see LICENSE.txt | |||||
| class Playlist(object): | |||||
| """Representation of a playlist of movies.""" | |||||
| def __init__(self, movies): | |||||
| """Create a playlist from the provided list of movies.""" | |||||
| self._movies = movies | |||||
| self._index = None | |||||
| def get_next(self): | |||||
| """Get the next movie in the playlist. Will loop to start of playlist | |||||
| after reaching end. | |||||
| """ | |||||
| # Check if no movies are in the playlist and return nothing. | |||||
| if len(self._movies) == 0: | |||||
| return None | |||||
| # Start at the first movie and increment through them in order. | |||||
| if self._index is None: | |||||
| self._index = 0 | |||||
| else: | |||||
| self._index += 1 | |||||
| # Wrap around to the start after finishing. | |||||
| if self._index >= len(self._movies): | |||||
| self._index = 0 | |||||
| return self._movies[self._index] | |||||
| def length(self): | |||||
| """Return the number of movies in the playlist.""" | |||||
| return len(self._movies) | |||||
| @@ -0,0 +1,74 @@ | |||||
| # Copyright 2015 Adafruit Industries. | |||||
| # Author: Tony DiCola | |||||
| # License: GNU GPLv2, see LICENSE.txt | |||||
| import os | |||||
| import subprocess | |||||
| import time | |||||
| class OMXPlayer(object): | |||||
| def __init__(self, config): | |||||
| """Create an instance of a video player that runs omxplayer in the | |||||
| background. | |||||
| """ | |||||
| self._process = None | |||||
| self._load_config(config) | |||||
| def _load_config(self, config): | |||||
| self._extensions = config.get('omxplayer', 'extensions') \ | |||||
| .translate(None, ' \t\r\n.') \ | |||||
| .split(',') | |||||
| self._extra_args = config.get('omxplayer', 'extra_args').split() | |||||
| self._sound = config.get('omxplayer', 'sound').lower() | |||||
| assert self._sound in ('hdmi', 'local', 'both'), 'Unknown omxplayer sound configuration value: {0} Expected hdmi, local, or both.'.format(self._sound) | |||||
| def supported_extensions(self): | |||||
| """Return list of supported file extensions.""" | |||||
| return self._extensions | |||||
| def play(self, movie, loop=False): | |||||
| """Play the provided movied file, optionally looping it repeatedly.""" | |||||
| self.stop(3) # Up to 3 second delay to let the old player stop. | |||||
| # Assemble list of arguments. | |||||
| args = ['omxplayer'] | |||||
| args.extend(['-o', self._sound]) # Add sound arguments. | |||||
| args.extend(self._extra_args) # Add extra arguments from config. | |||||
| if loop: | |||||
| args.append('--loop') # Add loop parameter if necessary. | |||||
| args.append(movie) # Add movie file path. | |||||
| # Run omxplayer process and direct standard output to /dev/null. | |||||
| self._process = subprocess.Popen(args, | |||||
| stdout=open(os.devnull, 'wb'), | |||||
| close_fds=True) | |||||
| def is_playing(self): | |||||
| """Return true if the video player is running, false otherwise.""" | |||||
| if self._process is None: | |||||
| return False | |||||
| self._process.poll() | |||||
| return self._process.returncode is None | |||||
| def stop(self, block_timeout_sec=None): | |||||
| """Stop the video player. block_timeout_sec is how many seconds to | |||||
| block waiting for the player to stop before moving on. | |||||
| """ | |||||
| # Stop the player if it's running. | |||||
| if self._process is not None and self._process.returncode is None: | |||||
| # process.kill() doesn't seem to work reliably if USB drive is | |||||
| # removed, instead just run a kill -9 on it. | |||||
| subprocess.call(['kill', '-9', str(self._process.pid)]) | |||||
| # If a blocking timeout was specified, wait up to that amount of time | |||||
| # for the process to stop. | |||||
| start = time.time() | |||||
| while self._process is not None and self._process.returncode is None: | |||||
| if (time.time() - start) >= block_timeout_sec: | |||||
| break | |||||
| time.sleep(0) | |||||
| # Let the process be garbage collected. | |||||
| self._process = None | |||||
| def create_player(config): | |||||
| """Create new video player based on omxplayer.""" | |||||
| return OMXPlayer(config) | |||||
| @@ -0,0 +1,46 @@ | |||||
| # Copyright 2015 Adafruit Industries. | |||||
| # Author: Tony DiCola | |||||
| # License: GNU GPLv2, see LICENSE.txt | |||||
| import glob | |||||
| from usb_drive_mounter import USBDriveMounter | |||||
| class USBDriveReader(object): | |||||
| def __init__(self, config): | |||||
| """Create an instance of a file reader that uses the USB drive mounter | |||||
| service to keep track of attached USB drives and automatically mount | |||||
| them for reading videos. | |||||
| """ | |||||
| self._load_config(config) | |||||
| self._mounter = USBDriveMounter(root=self._mount_path, | |||||
| readonly=self._readonly) | |||||
| self._mounter.start_monitor() | |||||
| def _load_config(self, config): | |||||
| self._mount_path = config.get('usb_drive', 'mount_path') | |||||
| self._readonly = config.getboolean('usb_drive', 'readonly') | |||||
| def search_paths(self): | |||||
| """Return a list of paths to search for files. Will return a list of all | |||||
| mounted USB drives. | |||||
| """ | |||||
| self._mounter.mount_all() | |||||
| return glob.glob(self._mount_path + '*') | |||||
| def is_changed(self): | |||||
| """Return true if the file search paths have changed, like when a new | |||||
| USB drive is inserted. | |||||
| """ | |||||
| return self._mounter.poll_changes() | |||||
| def idle_message(self): | |||||
| """Return a message to display when idle and no files are found.""" | |||||
| return 'Insert USB drive with compatible movies.' | |||||
| def create_file_reader(config): | |||||
| """Create new file reader based on mounting USB drives.""" | |||||
| return USBDriveReader(config) | |||||
| @@ -0,0 +1,80 @@ | |||||
| # Copyright 2015 Adafruit Industries. | |||||
| # Author: Tony DiCola | |||||
| # License: GNU GPLv2, see LICENSE.txt | |||||
| import glob | |||||
| import subprocess | |||||
| import time | |||||
| import pyudev | |||||
| class USBDriveMounter(object): | |||||
| """Service for automatically mounting attached USB drives.""" | |||||
| def __init__(self, root='/mnt/usbdrive', readonly=False): | |||||
| """Create an instance of the USB drive mounter service. Root is an | |||||
| optional parameter which specifies the location and file name prefix for | |||||
| mounted drives (a number will be appended to each mounted drive file | |||||
| name). Readonly is a boolean that indicates if the drives should be | |||||
| mounted as read-only or not (default false, writable). | |||||
| """ | |||||
| self._root = root | |||||
| self._readonly = readonly | |||||
| self._context = pyudev.Context() | |||||
| def remove_all(self): | |||||
| """Unmount and remove mount points for all mounted drives.""" | |||||
| for path in glob.glob(self._root + '*'): | |||||
| subprocess.call(['umount', '-l', path]) | |||||
| subprocess.call(['rm', '-r', path]) | |||||
| def mount_all(self): | |||||
| """Mount all attached USB drives. Readonly is a boolean that specifies | |||||
| if the drives should be mounted read only (defaults to false). | |||||
| """ | |||||
| self.remove_all() | |||||
| # Enumerate USB drive partitions by path like /dev/sda1, etc. | |||||
| nodes = [x.device_node for x in self._context.list_devices(subsystem='block', | |||||
| DEVTYPE='partition') \ | |||||
| if 'ID_BUS' in x and x['ID_BUS'] == 'usb'] | |||||
| # Mount each drive under the mount root. | |||||
| for i, node in enumerate(nodes): | |||||
| path = self._root + str(i) | |||||
| subprocess.call(['mkdir', path]) | |||||
| args = ['mount'] | |||||
| if self._readonly: | |||||
| args.append('-r') | |||||
| args.extend([node, path]) | |||||
| subprocess.check_call(args) | |||||
| def start_monitor(self): | |||||
| """Initialize monitoring of USB drive changes.""" | |||||
| self._monitor = pyudev.Monitor.from_netlink(self._context) | |||||
| self._monitor.filter_by('block', 'partition') | |||||
| self._monitor.start() | |||||
| def poll_changes(self): | |||||
| """Check for changes to USB drives. Returns true if there was a USB | |||||
| drive change, otherwise false. | |||||
| """ | |||||
| # Look for a drive change. | |||||
| device = self._monitor.poll(0) | |||||
| # If a USB drive changed (added/remove) remount all drives. | |||||
| if device is not None and device['ID_BUS'] == 'usb': | |||||
| return True | |||||
| # Else nothing changed. | |||||
| return False | |||||
| if __name__ == '__main__': | |||||
| # Run as a service that mounts all USB drives as read-only under the default | |||||
| # path of /mnt/usbdrive*. | |||||
| drive_mounter = USBDriveMounter(readonly=True) | |||||
| drive_mounter.mount_all() | |||||
| drive_mounter.start_monitor() | |||||
| print 'Listening for USB drive changes (press Ctrl-C to quite)...' | |||||
| while True: | |||||
| if drive_mounter.poll_changes(): | |||||
| print 'USB drives changed!' | |||||
| drive_mounter.mount_all() | |||||
| time.sleep(0) | |||||
| @@ -0,0 +1,93 @@ | |||||
| # Main configuration file for video looper. | |||||
| # You can change settings like what video player is used or where to search for | |||||
| # movie files. Lines that begin with # are comments that will be ignored. | |||||
| # Uncomment a line by removing its preceding # character. | |||||
| # Video looper configuration block follows. | |||||
| [video_looper] | |||||
| # What video player will be used to play movies. Can be either omxplayer or | |||||
| # hello_video. omxplayer can play common formats like avi, mov, mp4, etc. and | |||||
| # with full audio and video, but it has a small ~100ms delay between loops. | |||||
| # hello_video is a simpler player that doesn't do audio and only plays raw H264 | |||||
| # streams, but loops seemlessly. The default is omxplayer. | |||||
| video_player = omxplayer | |||||
| #video_player = hello_video | |||||
| # Where to find movie files. Can be either usb_drive or directory. When using | |||||
| # usb_drive any USB stick inserted in to the Pi will be automatically mounted | |||||
| # and searched for video files (only in the root directory). Alternatively the | |||||
| # directory option will search only a specified directory on the SD card for | |||||
| # movie files. Note that you change the directory by modifying the setting in | |||||
| # the [directory] section below. The default is usb_drive. | |||||
| file_reader = usb_drive | |||||
| #file_reader = directory | |||||
| # The rest of the configuration for video looper below is optional and can be | |||||
| # ignored. | |||||
| # Control whether informative messages about the current player state are | |||||
| # displayed, like the number of movies loaded or if it's waiting to load movies. | |||||
| # Default is true to display these messages, but can be set to false to disable | |||||
| # them entirely. | |||||
| osd = true | |||||
| #osd = false | |||||
| # Change the color of the background that is displayed behind movies (only works | |||||
| # with omxplayer). Provide 3 numeric values from 0 to 255 separated by a commma | |||||
| # for the red, green, and blue color value. Default is 0, 0, 0 or black. | |||||
| bgcolor = 0, 0, 0 | |||||
| # Change the color of the foreground text that is displayed with the on screen | |||||
| # display messages. Provide 3 numeric values in the same format as bgcolor | |||||
| # above. Default is 255, 255, 255 or white. | |||||
| fgcolor = 255, 255, 255 | |||||
| # Enable some output to standard output with program state. Good for debugging | |||||
| # but otherwise should be disabled. | |||||
| console_output = false | |||||
| #console_output = true | |||||
| # Directory file reader configuration follows. | |||||
| [directory] | |||||
| # The path to search for movies when using the directory file reader. | |||||
| path = /home/pi | |||||
| # USB drive file reader configuration follows. | |||||
| [usb_drive] | |||||
| # The path to mount new USB drives. A number will be appended to the path for | |||||
| # each mounted drive (i.e. /mnt/usbdrive0, /mnt/usbdrive1, etc.). | |||||
| mount_path = /mnt/usbdrive | |||||
| # Whether to mount the USB drives as readonly (true) or writable (false). It is | |||||
| # recommended to mount USB drives readonly for reliability. | |||||
| readonly = true | |||||
| # omxplayer configuration follows. | |||||
| [omxplayer] | |||||
| # List of supported file extensions. Must be comma separated and should not | |||||
| # include the dot at the start of the extension. | |||||
| extensions = avi, mov, mkv, mp4, m4v | |||||
| # Sound output for omxplayer, either hdmi, local, or both. When set to hdmi the | |||||
| # video sound will be played on the HDMI output, and when set to local the sound | |||||
| # will be played on the analog audio output. A value of both will play sound on | |||||
| # both HDMI and the analog output. The both value is the default. | |||||
| sound = both | |||||
| #sound = hdmi | |||||
| #sound = local | |||||
| # Any extra command line arguments to pass to omxplayer. It is not recommended | |||||
| # that you change this unless you have a specific need to do so! The audio and | |||||
| # video FIFO buffers are kept low to reduce clipping ends of movie at loop. | |||||
| extra_args = --no-osd --audio_fifo 0.01 --video_fifo 0.01 | |||||
| # hello_video player configuration follows. | |||||
| [hello_video] | |||||
| # List of supported file extensions. Must be comma separated and should not | |||||
| # include the dot at the start of the extension. | |||||
| extensions = h264 | |||||
| @@ -0,0 +1,215 @@ | |||||
| # Copyright 2015 Adafruit Industries. | |||||
| # Author: Tony DiCola | |||||
| # License: GNU GPLv2, see LICENSE.txt | |||||
| import atexit | |||||
| import ConfigParser | |||||
| import os | |||||
| import re | |||||
| import sys | |||||
| import time | |||||
| import pygame | |||||
| from model import Playlist | |||||
| # Basic video looper architecure: | |||||
| # | |||||
| # - VideoLooper class contains all the main logic for running the looper program. | |||||
| # | |||||
| # - Almost all state is configured in a .ini config file which is required for | |||||
| # loading and using the VideoLooper class. | |||||
| # | |||||
| # - VideoLooper has loose coupling with file reader and video player classes that | |||||
| # are used to find movie files and play videos respectively. The configuration | |||||
| # defines which file reader and video player module will be loaded. | |||||
| # | |||||
| # - A file reader module needs to define at top level create_file_reader function | |||||
| # that takes as a parameter a ConfigParser config object. The function should | |||||
| # return an instance of a file reader class. See usb_drive.py and directory.py | |||||
| # for the two provided file readers and their public interface. | |||||
| # | |||||
| # - Similarly a video player modules needs to define a top level create_player | |||||
| # function that takes in configuration. See omxplayer.py and hello_video.py | |||||
| # for the two provided video players and their public interface. | |||||
| # | |||||
| # - Future file readers and video players can be provided and referenced in the | |||||
| # config to extend the video player use to read from different file sources | |||||
| # or use different video players. | |||||
| class VideoLooper(object): | |||||
| def __init__(self, config_path): | |||||
| """Create an instance of the main video looper application class. Must | |||||
| pass path to a valid video looper ini configuration file. | |||||
| """ | |||||
| # Load the configuration. | |||||
| self._config = ConfigParser.SafeConfigParser() | |||||
| if len(self._config.read(config_path)) == 0: | |||||
| raise RuntimeError('Failed to find configuration file at {0}, is the application properly installed?'.format(config_path)) | |||||
| self._console_output = self._config.getboolean('video_looper', 'console_output') | |||||
| # Load configured video player and file reader modules. | |||||
| self._player = self._load_player() | |||||
| atexit.register(self._player.stop) # Make sure to stop player on exit. | |||||
| self._reader = self._load_file_reader() | |||||
| # Load other configuration values. | |||||
| self._osd = self._config.getboolean('video_looper', 'osd') | |||||
| # Parse string of 3 comma separated values like "255, 255, 255" into | |||||
| # list of ints for colors. | |||||
| self._bgcolor = map(int, self._config.get('video_looper', 'bgcolor') \ | |||||
| .translate(None, ',') \ | |||||
| .split()) | |||||
| self._fgcolor = map(int, self._config.get('video_looper', 'fgcolor') \ | |||||
| .translate(None, ',') \ | |||||
| .split()) | |||||
| # Initialize pygame and display a blank screen. | |||||
| pygame.display.init() | |||||
| pygame.font.init() | |||||
| pygame.mouse.set_visible(False) | |||||
| size = (pygame.display.Info().current_w, pygame.display.Info().current_h) | |||||
| self._screen = pygame.display.set_mode(size, pygame.FULLSCREEN) | |||||
| self._blank_screen() | |||||
| # Set other static internal state. | |||||
| self._extensions = self._player.supported_extensions() | |||||
| self._small_font = pygame.font.Font(None, 50) | |||||
| self._big_font = pygame.font.Font(None, 250) | |||||
| def _print(self, message): | |||||
| """Print message to standard output if console output is enabled.""" | |||||
| if self._console_output: | |||||
| print(message) | |||||
| def _load_player(self): | |||||
| """Load the configured video player and return an instance of it.""" | |||||
| module = self._config.get('video_looper', 'video_player') | |||||
| return __import__(module).create_player(self._config) | |||||
| def _load_file_reader(self): | |||||
| """Load the configured file reader and return an instance of it.""" | |||||
| module = self._config.get('video_looper', 'file_reader') | |||||
| return __import__(module).create_file_reader(self._config) | |||||
| def _build_playlist(self): | |||||
| """Search all the file reader paths for movie files with the provided | |||||
| extensions. | |||||
| """ | |||||
| # Get list of paths to search from the file reader. | |||||
| paths = self._reader.search_paths() | |||||
| # Enumerate all movie files inside those paths. | |||||
| movies = [] | |||||
| for ex in self._extensions: | |||||
| for path in paths: | |||||
| # Skip paths that don't exist or are files. | |||||
| if not os.path.exists(path) or not os.path.isdir(path): | |||||
| continue | |||||
| movies.extend(['{0}/{1}'.format(path.rstrip('/'), x) \ | |||||
| for x in os.listdir(path) \ | |||||
| if re.search('\.{0}$'.format(ex), x, | |||||
| flags=re.IGNORECASE)]) | |||||
| # Create a playlist with the sorted list of movies. | |||||
| return Playlist(sorted(movies)) | |||||
| def _blank_screen(self): | |||||
| """Render a blank screen filled with the background color.""" | |||||
| self._screen.fill(self._bgcolor) | |||||
| pygame.display.update() | |||||
| def _render_text(self, message, font=None): | |||||
| """Draw the provided message and return as pygame surface of it rendered | |||||
| with the configured foreground and background color. | |||||
| """ | |||||
| # Default to small font if not provided. | |||||
| if font is None: | |||||
| font = self._small_font | |||||
| return font.render(message, True, self._fgcolor, self._bgcolor) | |||||
| def _animate_countdown(self, playlist, seconds=10): | |||||
| """Print text with the number of loaded movies and a quick countdown | |||||
| message if the on screen display is enabled. | |||||
| """ | |||||
| # Print message to console with number of movies in playlist. | |||||
| message = 'Found {0} movie{1}.'.format(playlist.length(), | |||||
| 's' if playlist.length() >= 2 else '') | |||||
| self._print(message) | |||||
| # Do nothing else if the OSD is turned off. | |||||
| if not self._osd: | |||||
| return | |||||
| # Draw message with number of movies loaded and animate countdown. | |||||
| # First render text that doesn't change and get static dimensions. | |||||
| label1 = self._render_text(message + ' Starting playback in:') | |||||
| l1w, l1h = label1.get_size() | |||||
| sw, sh = self._screen.get_size() | |||||
| for i in range(seconds, 0, -1): | |||||
| # Each iteration of the countdown rendering changing text. | |||||
| label2 = self._render_text(str(i), self._big_font) | |||||
| l2w, l2h = label2.get_size() | |||||
| # Clear screen and draw text with line1 above line2 and all | |||||
| # centered horizontally and vertically. | |||||
| self._screen.fill(self._bgcolor) | |||||
| self._screen.blit(label1, (sw/2-l1w/2, sh/2-l2h/2-l1h)) | |||||
| self._screen.blit(label2, (sw/2-l2w/2, sh/2-l2h/2)) | |||||
| pygame.display.update() | |||||
| # Pause for a second between each frame. | |||||
| time.sleep(1) | |||||
| def _idle_message(self): | |||||
| """Print idle message from file reader.""" | |||||
| # Print message to console. | |||||
| message = self._reader.idle_message() | |||||
| self._print(message) | |||||
| # Do nothing else if the OSD is turned off. | |||||
| if not self._osd: | |||||
| return | |||||
| # Display idle message in center of screen. | |||||
| label = self._render_text(message) | |||||
| lw, lh = label.get_size() | |||||
| sw, sh = self._screen.get_size() | |||||
| self._screen.fill(self._bgcolor) | |||||
| self._screen.blit(label, (sw/2-lw/2, sh/2-lh/2)) | |||||
| pygame.display.update() | |||||
| def _prepare_to_run_playlist(self, playlist): | |||||
| """Display messages when a new playlist is loaded.""" | |||||
| # If there are movies to play show a countdown first (if OSD enabled), | |||||
| # or if no movies are available show the idle message. | |||||
| if playlist.length() > 0: | |||||
| self._animate_countdown(playlist) | |||||
| self._blank_screen() | |||||
| else: | |||||
| self._idle_message() | |||||
| def run(self): | |||||
| """Main program loop. Will never return!""" | |||||
| # Get playlist of movies to play from file reader. | |||||
| playlist = self._build_playlist() | |||||
| self._prepare_to_run_playlist(playlist) | |||||
| # Main loop to play videos in the playlist and listen for file changes. | |||||
| while True: | |||||
| # Load and play a new movie if nothing is playing. | |||||
| if not self._player.is_playing(): | |||||
| movie = playlist.get_next() | |||||
| if movie is not None: | |||||
| # Start playing the first available movie. | |||||
| self._print('Playing movie: {0}'.format(movie)) | |||||
| self._player.play(movie, loop=playlist.length() == 1) | |||||
| # Check for changes in the file search path (like USB drives added) | |||||
| # and rebuild the playlist. | |||||
| if self._reader.is_changed(): | |||||
| self._player.stop(3) # Up to 3 second delay waiting for old | |||||
| # player to stop. | |||||
| # Rebuild playlist and show countdown again (if OSD enabled). | |||||
| playlist = self._build_playlist() | |||||
| self._prepare_to_run_playlist(playlist) | |||||
| # Give the CPU some time to do other tasks. | |||||
| time.sleep(0) | |||||
| # Main entry point. | |||||
| if __name__ == '__main__': | |||||
| # Default config path to /boot. | |||||
| config_path = '/boot/video_looper.ini' | |||||
| # Override config path if provided as parameter. | |||||
| if len(sys.argv) == 2: | |||||
| config_path = sys.argv[1] | |||||
| # Create video looper and run it. | |||||
| videolooper = VideoLooper(config_path) | |||||
| videolooper.run() | |||||