Skip to content

Commit

Permalink
v1.0.14
Browse files Browse the repository at this point in the history
control the video looper via RPI GPIO pins (see section "control" below)
  • Loading branch information
tofuSCHNITZEL committed Nov 16, 2023
2 parents 5e55532 + 183d5fa commit 55cb6d8
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 62 deletions.
2 changes: 1 addition & 1 deletion Adafruit_Video_Looper/hello_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
2 changes: 1 addition & 1 deletion Adafruit_Video_Looper/image_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 27 additions & 19 deletions Adafruit_Video_Looper/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
# Author: Tony DiCola
# License: GNU GPLv2, see LICENSE.txt
import random
from os.path import basename
from typing import Optional, Union

random.seed()

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
Expand All @@ -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."""
Expand All @@ -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:
Expand Down Expand Up @@ -93,29 +98,32 @@ 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."""
return len(self._movies)

def clear_all_playcounts(self):
for movie in self._movies:
movie.clear_playcount()
movie.clear_playcount()
2 changes: 1 addition & 1 deletion Adafruit_Video_Looper/omxplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
99 changes: 68 additions & 31 deletions Adafruit_Video_Looper/video_looper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://www.raspberrypi.com/software/operating-systems/#raspberry-pi-os-legacy>
Currently only the __Legacy__ version of Raspberry Pi OS Lite is supported.
The last working image is this one:
<https://downloads.raspberrypi.com/raspios_oldstable_lite_armhf/images/raspios_oldstable_lite_armhf-2022-01-28/2022-01-28-raspios-buster-armhf-lite.zip>

For a detailed tutorial visit: <https://learn.adafruit.com/raspberry-pi-video-looper/installation>
There are also pre-compiled images available from <https://videolooper.de> (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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
23 changes: 18 additions & 5 deletions assets/video_looper.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]
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.13',
version = '1.0.14',
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 55cb6d8

Please sign in to comment.