Initial commit.
This commit is contained in:
parent
41a8747539
commit
e095906241
35
directory.py
Normal file
35
directory.py
Normal file
@ -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)
|
69
hello_video.py
Normal file
69
hello_video.py
Normal file
@ -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)
|
31
model.py
Normal file
31
model.py
Normal file
@ -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)
|
74
omxplayer.py
Normal file
74
omxplayer.py
Normal file
@ -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)
|
46
usb_drive.py
Normal file
46
usb_drive.py
Normal file
@ -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)
|
80
usb_drive_mounter.py
Normal file
80
usb_drive_mounter.py
Normal file
@ -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)
|
93
video_looper.ini
Normal file
93
video_looper.ini
Normal file
@ -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
|
215
video_looper.py
Normal file
215
video_looper.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user