diff --git a/Adafruit_Video_Looper/hello_video.py b/Adafruit_Video_Looper/hello_video.py index e79dc73..2c6fd2d 100644 --- a/Adafruit_Video_Looper/hello_video.py +++ b/Adafruit_Video_Looper/hello_video.py @@ -37,7 +37,7 @@ def play(self, movie, loop=None, **kwargs): args.append('--loop={0}'.format(loop)) #loop=0 means no loop - args.append(movie.filename) # Add movie file path. + args.append(movie.target) # 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'), diff --git a/Adafruit_Video_Looper/image_player.py b/Adafruit_Video_Looper/image_player.py index 48fe69b..293c377 100644 --- a/Adafruit_Video_Looper/image_player.py +++ b/Adafruit_Video_Looper/image_player.py @@ -41,7 +41,7 @@ def play(self, image, loop=None, **kwargs): if self._loop == 0: self._loop = 1 - imagepath = image.filename + imagepath = image.target if imagepath != "" and os.path.isfile(imagepath): self._blank_screen(False) diff --git a/Adafruit_Video_Looper/model.py b/Adafruit_Video_Looper/model.py index ddae9ad..eab333c 100644 --- a/Adafruit_Video_Looper/model.py +++ b/Adafruit_Video_Looper/model.py @@ -2,6 +2,7 @@ # Author: Tony DiCola # License: GNU GPLv2, see LICENSE.txt import random +from os.path import basename from typing import Optional, Union random.seed() @@ -9,9 +10,10 @@ class Movie: """Representation of a movie""" - def __init__(self, filename: str, title: Optional[str] = None, repeats: int = 1): + def __init__(self, target:str , title: Optional[str] = None, repeats: int = 1): """Create a playlist from the provided list of movies.""" - self.filename = filename + self.target = target + self.filename = basename(target) self.title = title self.repeats = int(repeats) self.playcount = 0 @@ -30,18 +32,20 @@ def finish_playing(self): self.playcount = self.repeats+1 def __lt__(self, other): - return self.filename < other.filename + return self.target < other.target def __eq__(self, other): if isinstance(other, str): return self.filename == other - return self.filename == other.filename + if isinstance(other, Movie): + return self.target == other.target + return False def __str__(self): return "{0} ({1})".format(self.filename, self.title) if self.title else self.filename def __repr__(self): - return repr((self.filename, self.title, self.repeats)) + return repr((self.target, self.filename, self.title, self.repeats, self.playcount)) class Playlist: """Representation of a playlist of movies.""" @@ -61,10 +65,11 @@ def get_next(self, is_random, resume = False) -> Movie: return None # Check if next movie is set and jump directly there: - if self._next is not None and self._next >= 0 and self._next <= self.length(): - self._index=self._next - self._next = None - return self._movies[self._index] + if self._next is not None: + next=self._next + self._next = None # reset next + self._index=self._movies.index(next) + return next # Start Random movie if is_random: @@ -93,24 +98,27 @@ def get_next(self, is_random, resume = False) -> Movie: return self._movies[self._index] - # sets next by filename or Movie object - def set_next(self, thing: Union[Movie, str]): + # sets next by filename or Movie object or index + def set_next(self, thing: Union[Movie, str, int]): if isinstance(thing, Movie): if (thing in self._movies): self._next(thing) elif isinstance(thing, str): if thing in self._movies: self._next = self._movies[self._movies.index(thing)] - - # sets next to the absolut index - def jump(self, index:int): + elif thing[0:1] in ("+","-"): + self._next = self._movies[(self._index+int(thing))%self.length()] + elif isinstance(thing, int): + if thing >= 0 and thing <= self.length(): + self._next = self._movies[thing] + else: + self._next = None self.clear_all_playcounts() - self._movies[self._index].finish_playing() - self._next = index - + self._movies[self._index].finish_playing() #set the current to max playcount so it will not get played again + # sets next relative to current index def seek(self, amount:int): - self.jump((self._index+amount)%self.length()) + self.set_next((self._index+amount)%self.length()) def length(self): """Return the number of movies in the playlist.""" @@ -118,4 +126,4 @@ def length(self): def clear_all_playcounts(self): for movie in self._movies: - movie.clear_playcount() + movie.clear_playcount() \ No newline at end of file diff --git a/Adafruit_Video_Looper/omxplayer.py b/Adafruit_Video_Looper/omxplayer.py index 2381744..c154f13 100644 --- a/Adafruit_Video_Looper/omxplayer.py +++ b/Adafruit_Video_Looper/omxplayer.py @@ -71,7 +71,7 @@ def play(self, movie, loop=None, vol=0): f.write(self._subtitle_header) f.write(movie.title) args.extend(['--subtitles', srt_path]) - args.append(movie.filename) # Add movie file path. + args.append(movie.target) # Add movie file path. # Run omxplayer process and direct standard output to /dev/null. # Establish input pipe for commands self._process = subprocess.Popen(args, diff --git a/Adafruit_Video_Looper/video_looper.py b/Adafruit_Video_Looper/video_looper.py index 48b4b86..5467d9e 100644 --- a/Adafruit_Video_Looper/video_looper.py +++ b/Adafruit_Video_Looper/video_looper.py @@ -11,8 +11,10 @@ import signal import time import pygame +import json import threading from datetime import datetime +import RPi.GPIO as GPIO from .alsa_config import parse_hw_device from .model import Playlist, Movie @@ -56,7 +58,7 @@ def __init__(self, config_path): self._osd = self._config.getboolean('video_looper', 'osd') self._is_random = self._config.getboolean('video_looper', 'is_random') self._resume_playlist = self._config.getboolean('video_looper', 'resume_playlist') - self._keyboard_control = self._config.getboolean('video_looper', 'keyboard_control') + self._keyboard_control = self._config.getboolean('control', 'keyboard_control') self._copyloader = self._config.getboolean('copymode', 'copyloader') # Get seconds for countdown from config self._countdown_time = self._config.getint('video_looper', 'countdown_time') @@ -111,6 +113,17 @@ def __init__(self, config_path): if self._keyboard_control: self._keyboard_thread = threading.Thread(target=self._handle_keyboard_shortcuts, daemon=True) self._keyboard_thread.start() + + pinMapSetting = self._config.get('control', 'gpio_pin_map', raw=True) + if pinMapSetting: + try: + self._pinMap = json.loads("{"+pinMapSetting+"}") + self._gpio_setup() + except Exception as err: + self._pinMap = None + self._print("gpio_pin_map setting is not valid and/or error with GPIO setup") + else: + self._pinMap = None def _print(self, message): """Print message to standard output if console output is enabled.""" @@ -315,38 +328,40 @@ def get_day_suffix(day): sw, sh = self._screen.get_size() for i in range(self._wait_time): - now = datetime.now() + if self._running: + now = datetime.now() - # Get the day suffix - suffix = get_day_suffix(int(now.strftime('%d'))) + # Get the day suffix + suffix = get_day_suffix(int(now.strftime('%d'))) - # Format the time and date strings - top_format = self._top_datetime_display_format.replace('%d{SUFFIX}', f'%d{suffix}') - bottom_format = self._bottom_datetime_display_format.replace('%d{SUFFIX}', f'%d{suffix}') + # Format the time and date strings + top_format = self._top_datetime_display_format.replace('%d{SUFFIX}', f'%d{suffix}') + bottom_format = self._bottom_datetime_display_format.replace('%d{SUFFIX}', f'%d{suffix}') - top_str = now.strftime(top_format) - bottom_str = now.strftime(bottom_format) + top_str = now.strftime(top_format) + bottom_str = now.strftime(bottom_format) - # Render the time and date labels - top_label = self._render_text(top_str, self._big_font) - bottom_label = self._render_text(bottom_str, self._medium_font) + # Render the time and date labels + top_label = self._render_text(top_str, self._big_font) + bottom_label = self._render_text(bottom_str, self._medium_font) - # Calculate the label positions - l1w, l1h = top_label.get_size() - l2w, l2h = bottom_label.get_size() + # Calculate the label positions + l1w, l1h = top_label.get_size() + l2w, l2h = bottom_label.get_size() - top_x = sw // 2 - l1w // 2 - top_y = sh // 2 - (l1h + l2h) // 2 - bottom_x = sw // 2 - l2w // 2 - bottom_y = top_y + l1h + 50 + top_x = sw // 2 - l1w // 2 + top_y = sh // 2 - (l1h + l2h) // 2 + bottom_x = sw // 2 - l2w // 2 + bottom_y = top_y + l1h + 50 - # Draw the labels to the screen - self._screen.fill(self._bgcolor) - self._screen.blit(top_label, (top_x, top_y)) - self._screen.blit(bottom_label, (bottom_x, bottom_y)) - pygame.display.update() + # Draw the labels to the screen - time.sleep(1) + self._screen.fill(self._bgcolor) + self._screen.blit(top_label, (top_x, top_y)) + self._screen.blit(bottom_label, (bottom_x, bottom_y)) + pygame.display.update() + + time.sleep(1) def _idle_message(self): """Print idle message from file reader.""" @@ -439,10 +454,25 @@ def _handle_keyboard_shortcuts(self): self._print("b was pressed. jumping back...") self._playlist.seek(-1) self._player.stop(3) - - - + + def _handle_gpio_control(self, pin): + if self._pinMap == None: + return + action = self._pinMap[str(pin)] + self._print("pin {} triggered: {}".format(pin, action)) + self._playlist.set_next(action) + self._player.stop(3) + + def _gpio_setup(self): + if self._pinMap == None: + return + GPIO.setmode(GPIO.BOARD) + for pin in self._pinMap: + GPIO.setup(int(pin), GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.add_event_detect(int(pin), GPIO.FALLING, callback=self._handle_gpio_control, bouncetime=200) + self._print("pin {} action set to: {}".format(pin, self._pinMap[pin])) + def run(self): """Main program loop. Will never return!""" # Get playlist of movies to play from file reader. @@ -508,17 +538,24 @@ def run(self): time.sleep(0.002) + self._print("run ended") + pygame.quit() + def quit(self, shutdown=False): """Shut down the program""" self._print("quitting Video Looper") + self._playbackStopped = True + self._running = False + pygame.event.post(pygame.event.Event(pygame.QUIT)) + if self._player is not None: self._player.stop() - pygame.quit() + if self._pinMap: + GPIO.cleanup() + if shutdown: os.system("sudo shutdown now") - self._running = False - def signal_quit(self, signal, frame): diff --git a/README.md b/README.md index 0403955..c570357 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ Works right out of the box, but also has a lot of customisation options to make If you miss a feature just post an issue here on Github. (https://github.com/adafruit/pi_video_looper) -Currently only the __Legacy__ version of Raspberry Pi OS Lite is supported. -You can download it from here: +Currently only the __Legacy__ version of Raspberry Pi OS Lite is supported. +The last working image is this one: + For a detailed tutorial visit: There are also pre-compiled images available from (but they might not always contain the latest version of pi_video_looper) ## Changelog +#### new in v1.0.14 + - control the video looper via RPI GPIO pins (see section "control" below) + #### new in v1.0.13 - Additional date/time functionality added. Allows you to add a second smaller line to display things like the date correctly. @@ -139,6 +143,10 @@ Note: files with the same name always get overwritten. * to reduce the wear of the SD card and potentially extend the lifespan of the player, you could enable the overlay filesystem via `raspi-config` and select Performance Options->Overlay Filesystem +### Control +The video looper can be controlled via keyboard input or via configured GPIO pins. +keyboard control is enabled by default via the ini setting `keyboard_control` + #### keyboard commands: The following keyboard commands are active by default (can be disabled in the [video_looper.ini](https://github.com/adafruit/pi_video_looper/blob/master/assets/video_looper.ini)): * "ESC" - stops playback and exits video_looper @@ -148,7 +156,24 @@ The following keyboard commands are active by default (can be disabled in the [v * "p" - Power off - stop playback and shutdown RPi * " " - (space bar) - Pause/Resume the omxplayer and imageplayer -#### troubleshooting: +#### GPIO control: +To enable GPIO control you need to set a GPIO pin mapping via the `gpio_pin_map` in the `control` section of the video_looper.ini. +Pins numbers are in "BOARD" numbering - see: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html +the pin mapping has the form: "pinnumber" : "action" +action can be one of the following: +* a filename as a string to play +* an absoulte index number (starting with 0) +* a string in the form of `+X` or `-X` (with X being an integer) for a relative jump + +Here are some examples that can be set: +* `"11" : 1` -> pin 11 will start the second file in the playlist +* `"13" : "-2"` -> pin 13 will jump back two files +* `"15" : "video.mp4"` -> pin 15 will start the file "video.mp4" (if it exists) +* `"16" : "+1"` -> pin 16 will start next file + +Note: to be used as an absolute index the action needs to be an integer not a string + +## Troubleshooting: * nothing happening (screen flashes once) when in copymode and new drive is plugged in? * check if you have the "password file" on your drive (see copymode explained above) * log output can be found in `/var/log/supervisor/`. Enable detailed logging in the video_looper.ini with console_output = true. diff --git a/assets/video_looper.ini b/assets/video_looper.ini index b4f2615..fc8f6d9 100644 --- a/assets/video_looper.ini +++ b/assets/video_looper.ini @@ -88,11 +88,6 @@ is_random = false resume_playlist = false #resume_playlist = true -# Control the program via keyboard -# If enabled, hit the ESC key to quit the program anytime (except countdown). See the readme for more keyboard commands. -keyboard_control = true -#keyboard_control = false - # Set the background to a custom image # This image is displayed between movies or images # an image will be scaled to the display resolution and centered. Use i.e. @@ -114,6 +109,24 @@ fgcolor = 255, 255, 255 console_output = false #console_output = true +[control] +# In this section all settings to interact with the looper are defined + +# Control the program via keyboard +# See the readme for the complete list of keyboard commands. +keyboard_control = true +#keyboard_control = false + +# This setting defines which Raspberry Pi GPIO pin (BOARD numbering!) will jump to which file in the playlist (first file has index 0) +# See: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html for info about the pin numbers +# the pins are pulled high so you need to connect your switch to the selected pin and Ground (e.g. pin 9) - there is some debouncing done in software +# The accepted settings are like this: "pinnumber" : videoindex or "pinnumber" : "filename" or "pinnumber" : "-1" or "pinnumber" : "+1" +# to enable GPIO set a gpio_pin_map like in the example below +gpio_pin_map = +#gpio_pin_map = "11" : 1, "13": 4, "15": "test.mp4", "16": "+2", "18": "-1" +# Example - Pin 11 should start 2nd video in the playlist, Pin 13 should start 5th video, pin 15 should play file with name "test.mp4", +# pin 16 should jump 2 videos ahead and pin 18 should jump one video back + # USB drive file reader configuration follows. [usb_drive] diff --git a/setup.py b/setup.py index 58bd4f4..247d76e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name = 'Adafruit_Video_Looper', - version = '1.0.13', + version = '1.0.14', author = 'Tony DiCola', author_email = 'tdicola@adafruit.com', 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.',