This is part of a series of posts on the design and technical steps of creating Himblick, a digital signage box based on the Raspberry Pi 4.
Finally, we have enough pieces to start working on the media player. It's been way more work that expected getting to this point, and I hope that this series of posts could help others getting a faster start.
To begin with, we'd like to be able to show:
- PDF files automatically looping through the pages
- Image galleries automatically looping
- Looping videos
- ODP presentations
Configuring the screen
The first thing to do on startup is to configure the screen based on the himblick.conf settings:
def configure_screen(self):
"""
Configure the screen based on himblick.conf
"""
# Set screen orientation
orientation = self.settings.general("screen orientation")
if orientation:
run(["xrandr", "--orientation", orientation])
mode = self.settings.general("screen mode")
if mode:
res = run(["xrandr", "--query"], capture_output=True, text=True)
re_output = re.compile(r"^(\S+) connected ")
for line in res.stdout.splitlines():
mo = re_output.match(line)
if mo:
output_name = mo.group(1)
break
else:
output_name = None
run(["xrandr", "--output", output_name, "--mode", mode])
This had the extra complication of needing to parse xrandr --query
output to
figure out the name of the HDMI output in use, since the RaspberryPi 4 has two
of them. It would be nice if xrandr could join the ranks of tools with a
machine parsable output, like for example lsblk is doing.
Scanning the media directory
The next step is finding what to play. We scan the media directory looking at file mimetypes, to avoid having to hardcode all the possible file extension that image and video files can have. Then we group media by type, and pick the group with the most recent files:
def find_presentation(self, path):
"""
Find the presentation to play from a given media directory
"""
if not os.path.isdir(path):
return None
pdf = PDFPresentation()
videos = VideoPresentation()
images = ImagePresentation()
odp = ODPPresentation()
all_players = [pdf, videos, images, odp]
for fn in os.listdir(path):
abspath = os.path.abspath(os.path.join(path, fn))
base, ext = os.path.splitext(fn)
mimetype = mimetypes.types_map.get(ext)
if mimetype is None:
log.info("%s: mime type unknown", fn)
continue
else:
log.info("%s: mime type %s", fn, mimetype)
if mimetype == "application/pdf":
pdf.add(abspath)
elif mimetype.startswith("image/"):
images.add(abspath)
elif mimetype.startswith("video/"):
videos.add(abspath)
elif mimetype == "application/vnd.oasis.opendocument.presentation":
odp.add(abspath)
player = max(all_players, key=lambda x: x.mtime)
if not player:
return None
return player
Caffeinate
We don't want power management to kick in and turn our signage screens black, so we are running all the media players under caffeine.
We automated with a simple subprocess.run
wrapper. Note the --
to
prevent caffeinate from choking on the options passed to the actual media
players:
def run(cmd: List[str], check: bool = True, **kw) -> subprocess.CompletedProcess:
"""
Logging wrapper to subprocess.run.
Also, default check to True.
"""
log.info("Run %s", " ".join(shlex.quote(x) for x in cmd))
return subprocess.run(cmd, check=check, **kw)
class Presentation:
"""
Base class for all presentation types
"""
def run_player(self, cmd, **kw):
"""
Run a media player command line, performing other common actions if
needed
"""
# Run things under caffeinate
# See also: https://stackoverflow.com/questions/10885337/inhibit-screensaver-with-python
cmd = ["caffeinate", "--"] + cmd
run(cmd, **kw)
Showing PDFs
okular
seems to be the only PDF reader in Debian
that can be convinced
to do looping non interactive full screen presentations,
with only a bit of tampering with its configuration files:
class PDFPresentation(SingleFileMixin, Presentation):
def run(self):
log.info("%s: PDF presentation", self.fname)
confdir = os.path.expanduser("~/.config")
os.makedirs(confdir, exist_ok=True)
# TODO: configure slide advance time
# Configure okular
with open(os.path.expanduser(os.path.join(confdir, "okularpartrc")), "wt") as fd:
print("[Core Presentation]", file=fd)
print("SlidesAdvance=true", file=fd)
print("SlidesAdvanceTime=2", file=fd)
print("SlidesLoop=true", file=fd)
print("[Dlg Presentation]", file=fd)
print("SlidesShowProgress=false", file=fd)
# print("SlidesTransition=GlitterRight", file=fd)
# Silence a too-helpful first-time-run informational message
with open(os.path.expanduser(os.path.join(confdir, "okular.kmessagebox")), "wt") as fd:
print("[General]", file=fd)
print("presentationInfo=4", file=fd)
# Remove state of previous okular runs, so presentations begin at the
# beginning
docdata = os.path.expanduser("~/.local/share/okular/docdata/")
if os.path.isdir(docdata):
shutil.rmtree(docdata)
self.run_player(["okular", "--presentation", "--", self.fname])
I was surprised at how looping a PDF presentation doesn't seem to be a well
supported use case in PDF viewers. If it's somewhat painful to do it in okular,
it's downright impossible to do it with evince: try evince --fullscreen
--presentation
: slides won't advance, and it still shows a toolbar!
Showing images
class ImagePresentation(FileGroupMixin, Presentation):
def run(self):
self.files.sort()
log.info("Image presentation of %d images", len(self.files))
with tempfile.NamedTemporaryFile("wt") as tf:
for fname in self.files:
print(fname, file=tf)
tf.flush()
# TODO: adjust slide advance time
self.run_player(["feh", "--filelist", tf.name, "--fullscreen",
"--hide-pointer", "--slideshow-delay", "1.5"])
feh does everything needed and more. It seems to support our use case explictly, with useful knobs exposed on the command line, clean, straightforward, beautiful!
Showing videos
Most internet posts about playing media on Raspberry Pi, suggest omxplayer. After trying it it looked quite worrysome, as it seemed to fail with any media format not supported in hardware, there did not seem to be a way to ask it whether a file would be in a playable format or not, and one of the failures left the screen in the wrong resolution.
We would like the media player to be able to play the widest possible range of media, hardware accelerated if possible, software if not.
Luckily, it turned out that vlc can use the Raspberry Pi 4 hardware acceleration, and playing a 1920x1080 video full screen on it would consume only 4% of CPU, which is the same that omxplayer was using.
That was very relieving, as vlc can also play a wide range of media, has excellent support for gapless looping, can be invoked without a UI, and can even do playlists of multiple media.
Here is the corresponding player code:
class VideoPresentation(FileGroupMixin, Presentation):
def run(self):
self.files.sort()
log.info("Video presentation of %d videos", len(self.files))
with tempfile.NamedTemporaryFile("wt", suffix=".vlc") as tf:
for fname in self.files:
print(fname, file=tf)
tf.flush()
self.run_player(
["cvlc", "--no-audio", "--loop", "--fullscreen",
"--video-on-top", "--no-video-title-show", tf.name])
Showing presentations
The code here is quite straightforward, but it took a while to put together that command line:
class ODPPresentation(SingleFileMixin, Presentation):
def run(self):
log.info("%s: ODP presentation", self.fname)
self.run_player(["loimpress", "--nodefault", "--norestore", "--nologo", "--nolockcheck", "--show", self.fname])
I was surprised that I could not find a way to tell Impress to just play a presentation without other things getting in the way. Even like that, there is a moment in which the UI can be seen to come up on the screen before being covered by the full screen presentation.
There is also no way to force a presentation to loop or to advance slides after a given timeout: both features need to be set in the presentation itself.
People will have to do a test run of their presentations with a checklist before putting them on the player. It would have been nice to have an easy way to guarantee that a presentation wouldn't get stuck on the player.
That is not a requirement for now anyway. If it ever becomes one, I guess we
can always write code to check and tweak the .odp
presentation file: it's
thankfully a well known and open format.
Auditing recommends
So far we installed everything with --no-install-recommends
, but it's risky
to do so when dealing with packages with many dependencies like vlc, okular,
and impress.
Aptitude offers the possibility to audit
recommends: Views / Audit Recommendations
will show a list of recommended but
not installed packages.
That turned out some font packages that it's maybe nice to have, and libreoffice-avmedia-backend-vlc
that may come in handy if people decide to play presentations with embedded
videos.
Existing presentation software
Our needs for media playing so far have been simple. Should they become more complex, here are some pointers to existing, more featureful projects: