Skip to content

Commit

Permalink
v1.0.5 release (adafruit#78)
Browse files Browse the repository at this point in the history
* Add titles and fixed playlists

* Pass directly a Movie object to VideoPlayer.play

* Update config

* Fix end time

* Remove specific config keys which can go to extra_args

* removed playlist type setting

* fix for player breaking with no usb stick

* Add titles and fixed playlists (adafruit#67)

* Add titles and fixed playlists

* v1.0.5

* Pass directly a Movie object to VideoPlayer.play

* Update config

* Fix end time

* Remove specific config keys which can go to extra_args

* Add font-size in extra_args

* use play all the files if playlist file does not work out

* v1.0.5 updated readme
  • Loading branch information
tofuSCHNITZEL authored Nov 6, 2019
1 parent e045e42 commit a9d7501
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 25 deletions.
6 changes: 4 additions & 2 deletions Adafruit_Video_Looper/hello_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ def supported_extensions(self):
"""Return list of supported file extensions."""
return self._extensions

def play(self, movie, loop=0, **kwargs):
def play(self, movie, loop=None, **kwargs):
"""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 is None:
loop = movie.repeats
if loop <= -1:
args.append('--loop') # Add loop parameter if necessary.
elif loop > 0:
args.append('--loop={0}'.format(loop))
#loop=0 means no loop

args.append(movie) # Add movie file path.
args.append(movie.filename) # 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'),
Expand Down
16 changes: 9 additions & 7 deletions Adafruit_Video_Looper/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
# Author: Tony DiCola
# License: GNU GPLv2, see LICENSE.txt
import random
from typing import Optional


class Movie:
"""Representation of a movie"""

def __init__(self, filename: str, repeats: int = 1):
def __init__(self, filename: str, title: Optional[str] = None, repeats: int = 1):
"""Create a playlist from the provided list of movies."""
self.filename = filename
self.title = title
self.repeats = int(repeats)
self.playcount = 0

Expand All @@ -29,29 +32,28 @@ def __eq__(self, other):
return self.filename == other.filename

def __str__(self):
return self.filename
return "{0} ({1})".format(self.filename, self.title) if self.title else self.filename

def __repr__(self):
return repr((self.filename, self.repeats))
return repr((self.filename, self.title, self.repeats))

class Playlist:
"""Representation of a playlist of movies."""

def __init__(self, movies, is_random):
def __init__(self, movies):
"""Create a playlist from the provided list of movies."""
self._movies = movies
self._index = None
self._is_random = is_random

def get_next(self) -> Movie:
def get_next(self, is_random) -> Movie:
"""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 Random movie
if self._is_random:
if is_random:
self._index = random.randrange(0, self.length())
else:
# Start at the first movie and increment through them in order.
Expand Down
37 changes: 33 additions & 4 deletions Adafruit_Video_Looper/omxplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# Author: Tony DiCola
# License: GNU GPLv2, see LICENSE.txt
import os
import shutil
import subprocess
import tempfile
import time


Expand All @@ -13,32 +15,59 @@ def __init__(self, config):
background.
"""
self._process = None
self._temp_directory = None
self._load_config(config)

def __del__(self):
if self._temp_directory:
shutil.rmtree(self._temp_directory)

def _get_temp_directory(self):
if not self._temp_directory:
self._temp_directory = tempfile.mkdtemp()
return self._temp_directory

def _load_config(self, config):
self._extensions = config.get('omxplayer', 'extensions') \
.translate(str.maketrans('', '', ' \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)
self._show_titles = config.getboolean('omxplayer', 'show_titles')
if self._show_titles:
title_duration = config.getint('omxplayer', 'title_duration')
if title_duration >= 0:
m, s = divmod(title_duration, 60)
h, m = divmod(m, 60)
self._subtitle_header = '00:00:00,00 --> {:d}:{:02d}:{:02d},00\n'.format(h, m, s)
else:
self._subtitle_header = '00:00:00,00 --> 99:59:59,00\n'

def supported_extensions(self):
"""Return list of supported file extensions."""
return self._extensions

def play(self, movie, loop=0, vol=0):
"""Play the provided movied file, optionally looping it repeatedly."""
def play(self, movie, loop=None, vol=0):
"""Play the provided movie 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 vol is not 0:
args.extend(['--vol', str(vol)])
if loop is None:
loop = movie.repeats
if loop <= -1:
args.append('--loop') # Add loop parameter if necessary.
args.append(movie) # Add movie file path.
args.append('--loop') # Add loop parameter if necessary.
if self._show_titles and movie.title:
srt_path = os.path.join(self._get_temp_directory(), 'video_looper.srt')
with open(srt_path, 'w') as f:
f.write(self._subtitle_header)
f.write(movie.title)
args.extend(['--subtitles', srt_path])
args.append(movie.filename) # Add movie file path.
# Run omxplayer process and direct standard output to /dev/null.
self._process = subprocess.Popen(args,
stdout=open(os.devnull, 'wb'),
Expand Down
28 changes: 28 additions & 0 deletions Adafruit_Video_Looper/playlist_builders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
import re
import urllib.parse

from .model import Playlist, Movie


def build_playlist_m3u(playlist_path: str):
playlist_dirname = os.path.dirname(playlist_path)
movies = []

title = None

with open(playlist_path) as f:
for line in f:
if line.startswith('#'):
if line.startswith('#EXTINF'):
matches = re.match(r'^#\w+:\d+(?:\s*\w+\=\".*\")*,(.*)$', line)
if matches:
title = matches[1]
else:
path = urllib.parse.unquote(line.rstrip())
if not os.path.isabs(path):
path = os.path.join(playlist_dirname, path)
movies.append(Movie(path, title))
title = None

return Playlist(movies)
62 changes: 53 additions & 9 deletions Adafruit_Video_Looper/video_looper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import threading

from .model import Playlist, Movie
from .playlist_builders import build_playlist_m3u


# Basic video looper architecure:
#
Expand Down Expand Up @@ -120,14 +122,55 @@ def _load_bgimage(self):
image = pygame.transform.scale(image, self._size)
return image

def _is_number(iself, s):
def _is_number(self, s):
try:
float(s)
return True
except ValueError:
return False

def _build_playlist(self):
"""Try to build a playlist (object) from a playlist (file).
Falls back to an auto-generated playlist with all files.
"""
if self._config.has_option('playlist', 'path'):
playlist_path = self._config.get('playlist', 'path')
if playlist_path != "":
if os.path.isabs(playlist_path):
if not os.path.isfile(playlist_path):
self._print('Playlist path {0} does not exist.'.format(playlist_path))
return self._build_playlist_from_all_files()
#raise RuntimeError('Playlist path {0} does not exist.'.format(playlist_path))
else:
paths = self._reader.search_paths()

if not paths:
return Playlist([])

for path in paths:
maybe_playlist_path = os.path.join(path, playlist_path)
if os.path.isfile(maybe_playlist_path):
playlist_path = maybe_playlist_path
self._print('Playlist path resolved to {0}.'.format(playlist_path))
break
else:
self._print('Playlist path {0} does not resolve to any file.'.format(playlist_path))
return self._build_playlist_from_all_files()
#raise RuntimeError('Playlist path {0} does not resolve to any file.'.format(playlist_path))

basepath, extension = os.path.splitext(playlist_path)
if extension == '.m3u' or extension == '.m3u8':
return build_playlist_m3u(playlist_path)
else:
self._print('Unrecognized playlist format {0}.'.format(extension))
return self._build_playlist_from_all_files()
#raise RuntimeError('Unrecognized playlist format {0}.'.format(extension))
else:
return self._build_playlist_from_all_files()
else:
return self._build_playlist_from_all_files()

def _build_playlist_from_all_files(self):
"""Search all the file reader paths for movie files with the provided
extensions.
"""
Expand All @@ -148,7 +191,8 @@ def _build_playlist(self):
repeat = repeatsetting.group(1)
else:
repeat = 1
movies.append(Movie('{0}/{1}'.format(path.rstrip('/'), x), repeat))
basename, extension = os.path.splitext(x)
movies.append(Movie('{0}/{1}'.format(path.rstrip('/'), x), basename, repeat))

# Get the video volume from the file in the usb key
sound_vol_file_path = '{0}/{1}'.format(path.rstrip('/'), self._sound_vol_file)
Expand All @@ -158,7 +202,7 @@ def _build_playlist(self):
if self._is_number(sound_vol_string):
self._sound_vol = int(float(sound_vol_string))
# Create a playlist with the sorted list of movies.
return Playlist(sorted(movies), self._is_random)
return Playlist(sorted(movies))

def _blank_screen(self):
"""Render a blank screen filled with the background color."""
Expand Down Expand Up @@ -278,7 +322,7 @@ def run(self):
# Get playlist of movies to play from file reader.
playlist = self._build_playlist()
self._prepare_to_run_playlist(playlist)
movie = playlist.get_next()
movie = playlist.get_next(self._is_random)
# Main loop to play videos in the playlist and listen for file changes.
while self._running:
# Load and play a new movie if nothing is playing.
Expand All @@ -287,10 +331,10 @@ def run(self):

if movie.playcount >= movie.repeats:
movie.clear_playcount()
movie = playlist.get_next()
movie = playlist.get_next(self._is_random)
elif self._player.can_loop_count() and movie.playcount > 0:
movie.clear_playcount()
movie = playlist.get_next()
movie = playlist.get_next(self._is_random)

movie.was_played()

Expand All @@ -310,7 +354,7 @@ def run(self):
# Start playing the first available movie.
self._print('Playing movie: {0} {1}'.format(movie, infotext))
# todo: maybe clear screen to black so that background (image/color) is not visible for videos with a resolution that is < screen resolution
self._player.play(movie.filename, loop=-1 if playlist.length()==1 else movie.repeats, vol = self._sound_vol)
self._player.play(movie, loop=-1 if playlist.length()==1 else None, vol = self._sound_vol)

# Check for changes in the file search path (like USB drives added)
# and rebuild the playlist.
Expand All @@ -322,7 +366,7 @@ def run(self):
# Rebuild playlist and show countdown again (if OSD enabled).
playlist = self._build_playlist()
self._prepare_to_run_playlist(playlist)
movie = playlist.get_next()
movie = playlist.get_next(self._is_random)

# Give the CPU some time to do other tasks.
time.sleep(0.002)
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ If you miss a feature just post an issue on github. (https://github.com/adafruit

## Changelog

#### new in v1.0.5

- Support for M3U playlists.
To be enabled by specifying a playlist path in config key `playlist.path`. It can be absolute, or relative to the `file_reader`'s search directories (`directory`: path given, `usb_drive`: all USB drives' root).
Paths in the playlist can be absolute, or relative to the playlist's path.
Playlists can include a title for each item (`#EXTINF` directive); see next point.
If something goes wrong with the playlist (file not found etc.) it will fall back to just play all files in the `file_reader` directory. (enable `console_output` for more info)
- Support for video titles (OMXPlayer only).
Can display a text on top of the videos.
To be enabled by config key `omxplayer.show_titles`.
Without a playlist file, titles are simply the videos' filename (without extension).
If a M3U playlist is used, then titles come from the playlist instead.

#### new in v1.0.4
- new keyboard shortcut "k"
skips the playback of current video (if a video is set to repeat it only skips one iteration)
Expand Down
20 changes: 19 additions & 1 deletion assets/video_looper.ini
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ copyloader = false
password = videopi


[playlist]
# This setting allows for a fixed playlist.

# Path to the playlist file.
# If you enter a relative path (not starting with /) it is considered relative to the selected file_reader path (directory or USB drive).
# Leave empty to not use a playlist and play all the files in the file_reader path (directory or USB drive).
path =
#path = playlist.m3u

# omxplayer configuration follows.
[omxplayer]

Expand All @@ -141,10 +150,19 @@ sound = both
# its --vol option which takes a value in millibels).
sound_vol_file = sound_volume

# Fixed playlists may embed titles, which can be shown. See playlist section above.
# If no fixed playlist is given, titles are simply filenames without extensions.
show_titles = false
#show_titles = true

# Title duration in seconds. -1 means endless.
title_duration = 10

# 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
# Run 'omxplayer -h' to have the full list of parameters.
extra_args = --no-osd --audio_fifo 0.01 --video_fifo 0.01 --align center --font-size 55

# hello_video player configuration follows.
[hello_video]
Expand Down
2 changes: 1 addition & 1 deletion reload.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Make sure script is run as root.
if [ "$(id -u)" != "0" ]; then
echo "Must be run as root with sudo! Try: sudo ./enable.sh"
echo "Must be run as root with sudo! Try: sudo ./reload.sh"
exit 1
fi

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages

setup(name = 'Adafruit_Video_Looper',
version = '1.0.4',
version = '1.0.5',
author = 'Tony DiCola',
author_email = '[email protected]',
description = 'Application to turn your Raspberry Pi into a dedicated looping video playback device, good for art installations, information displays, or just playing cat videos all day.',
Expand Down

0 comments on commit a9d7501

Please sign in to comment.