Raspberry Pi Media Player

Thursday October 10 2024, 1832 words — Building a minimal Raspberry Pi based audio and video player for exhibitions.
Filed under: Software

A pretty common task for exhibitions I support is to have some kind of tiny unobtrusive media player box that can play a bunch of video or audio files on loop for anything from a small listening station with some headphones to a 4k projector showing a feature video.

Since this has come up again and again for me and I’ve had multiple different versions of a setup like this for which I’ve somehow managed to loose all the source files I’ve decided to write this blog post in the hopes that when I have to do this again I’ll actually remember how I did it this time.

I’m not gonna go into every detail here, like how to set up systemd services, but if you want to take a closer look all the scripts and files are available for download at the end of the article.


Requirements

The main requirement for this setup is reliability and ease of use for the artists I work with. Everything needs to be plug and play. There should never be the need to configure anything or see as much as a desktop or even command line. There should never be a situation where the thing doesn’t do the thing simply by powering it on. It needs to be possible to add media files to it simply by copying them onto the SD card.

It also needs to survive having it’s plug pulled or power turned off repeatedly possibly for weeks on end as stuff is shut down at night. If there is ever a power interruption the video/audio should start back up as quickly as possible without projecting a linux boot log onto a giant screen.

And, for my own personal sanity I want it to be a tiny system that can quickly be built from scratch using some kind of build script. The smaller the image file the better so I don’t have to wait ages while writing it onto SD Cards over and over. No ssh-ing into stuff to finish the setup. Once the card is in the Pi it needs to be ready to go.

Choices

I decided to base the system around Arch Linux ARM, since I’ve been using that on Raspberry Pis for a while and I’m very familiar with it. And since I also run Arch Linux on my workstation all the required build tools are easy to set up.

While I could have probably chosen something like Alpine Linux to get an ever smaller system the convenience of having pre-built packages of VLC optimized for hardware accelerated playback on the Raspberry Pi was worth it for me.

For the media player itself I chose to go with VLC since it currently seems to offer the best support for hardware video decoding on the later Pi models. I’ve also tested mplayer and omxplayer but VLC seems to have the best performance over all.

Because I wanted the system to be as reliable as possible I chose to run everything off of a read-only root filesystem. While Arch Linux isn’t really made for that it seems to work fine.

One thing worth mentioning is the choice of file system for the media partition.
I needed to have a partition on the SD card that would hold the video and audio files to be played back. This partition needs to be both able to be read and written from a variety of operating systems (specifically Linux and Mac OS), and also be able to hold files larger than 4GB. The only viable choice here seems to be ExFAT which seems to have good enough support under Linux by now to be viable for this kinda use which is great to see.

To keep the image that needs to be burned onto the SD card as small as possible I decided to not include the media partition in the image but create it automatically on first boot instead and make it take up whatever space is left on the card.

Prerequisites

Since we’ll be making use of pacstrap to build the system you’ll need to install arch-install-scripts if you’re following along. You’ll also need qemu-user-static to run ARM binaries in a chroot during installation.


Base system setup

While it is possible to just download a pre-built OS image for the Raspberry Pi from archlinuxarm.org I decided to build my own system with pacstrap. This results in a much smaller and more streamlined installation because you have a lot more control over which packages to install. It also allows you to set up all the packages you want in one go, without having to actually install a package manager on the target system.

Creating and mounting the image

# setup.sh, partial

truncate -s 2500M pi.img

sfdisk pi.img << EOF
start=2048, size=256M, type=0c
type=83
EOF

lodev=$(sudo losetup -Pf --show "$(realpath pi.img)")
sudo mkfs.vfat -n "BOOT" "${lodev}p1"
sudo mkfs.ext4 -L "ROOT" "${lodev}p2"

mkdir -p root

sudo mount "${lodev}p2" root
sudo mkdir -p root/boot/
sudo mount "${lodev}p1" root/boot/

As a first step we use truncate to create a sparse 2.5Gb image file and partition it using sfdisk. sfdisk is the “scriptable” version of fdisk that just reads instructions for creating partitions line by line from standard input. In the first line we create a 256Mb FAT (type 0x0C) boot partition, in the second line we create a “Linux” (type 0x83) partition that’s gonna take up the rest of the image.

Next we set up a loopback device from the image file, save its name in a variable for later, and create the file systems using mkfs.

Note that we’re assigning labels to both partitions during formatting as this will allow us to mount them without having to know their exact device names later on.

For now we’re just mounting them both to a newly created directory to finish the rest of the setup.

Installing the software

# setup.sh, continued

sudo pacstrap -GMcC pacman.conf root linux-rpi systemd systemd-sysvcompat exfat-utils vlc-rpi gnu-free-fonts

Hre we use pacstrap to install a very bare bones system into the mounted image.

The only packages we install are the linux-rpi kernel, systemd and systemd-sysvcompat for system management and init, exfat-utils to create the media partition, and vlc-rpi for playing back the videos and audios. Note that we’re not installing the base package and relying on dependency resolution instead to get all the packages this minimal set of software needs to run.

One exception to that is the gnu-free-fonts package. You can theoretically run VLC without any fonts installed but it does complain it a lot when you do so. Also you might be in a situation where you want to show subtitles.

We’re also passing a bunch of extra options to pacstrap to minimize what it does on the newly installed system. -G and -M keep it from copying the hosts mirrorlist and keyring onto new system and -c makes it use the host package cache instead instead of that of the target. This speeds up rebuilding of the system and keeps cached package files out of the image. Since there will be no package manager installed on the target system there is no need for any of these things.

We also specify a custom pacman.conf file which contains the settings and mirrors for setting up the new system.

# pacman.conf

[options]
Architecture = aarch64
CheckSpace
ParallelDownloads = 5

SigLevel = Never

[core]
Server = http://mirror.archlinuxarm.org/$arch/$repo

[extra]
Server = http://mirror.archlinuxarm.org/$arch/$repo

[alarm]
Server = http://mirror.archlinuxarm.org/$arch/$repo

[aur]
Server = http://mirror.archlinuxarm.org/$arch/$repo

As you can see I’ve disabled signature checks (SigLevel = Never) for downloaded packages which is kinda naughty but keeps me from having to set up the Arch Linux ARM keyring on my machine.

Post install setup

After packstrap has finished there are still a few tasks we need to take care of.

# setup.sh, continued

sudo install -m 644 files/partcheck.service root/etc/systemd/system/
sudo install -m 644 files/play.service root/etc/systemd/system/
sudo install files/partcheck.sh root/usr/local/bin/
sudo install files/play.sh root/usr/local/bin/

sudo install files/postinstall.sh root/
sudo arch-chroot root bash /postinstall.sh

First we install the required files for the media player and media partition creation service into the system, followed by a postinstall script which we then run inside the new system using arch-chroot.

#!/bin/bash

# postinstall.sh

useradd -m -G audio,video mediaplayer

echo "blacklist snd_bcm2835" >> /etc/modprobe.d/alsa-blacklist.conf

systemctl disable getty@tty1.service
sed -i "s/ rw / ro /" /boot/cmdline.txt
sed -i "s/console=tty1/logo.nologo/" /boot/cmdline.txt
sed -i 's/^#.*\(dtoverlay=disable-.*\)/\1/' /boot/config.txt
sed -i 's/\(.*\)_auto_detect=1/\1_auto_detect=0/' /boot/config.txt

mkdir -p /var/lib/systemd/linger

systemctl enable partcheck
systemctl enable play

rm "$0"

This script creates a new user for running the media player service, blacklists the builtin Raspberry Pi audio interface and then changes a bunch of boot options to make the root file system read only, disable graphical output during boot, the default login prompt, and wifi and bluetooth hardware.

Next we create a directory to keep loginctl from failing on a readonly file system and enable our media player and partition checking service.

After the work of the postinstall script is done it deletes itself.

fstab

# setup.sh, continued

sudo install -m 644 files/fstab root/etc/fstab

After all of that is done we copy a fresh fstab into the new system.

# /etc/fstab

# Static information about the filesystems.
# See fstab(5) for details.

# <file system> <dir> <type> <options>  <dump> <pass>
LABEL=ROOT      /     ext4   ro         0      1
LABEL=BOOT      /boot vfat   ro,        0      2
LABEL=Media     /mnt  exfat  nofail,ro  0      2

It uses the labels we have previously assigned to out partitions and mounts them all as readonly,that way we don’t have to worry about anything corrupting when the Pi gets turned off.

The “Media” partition also gets marked as “nofail” to make sure the system still boots without it present during the first bootup.


The media partition creation script

#!/bin/bash

# partcheck.sh

if [ ! -b /dev/mmcblk?p3 ]
then
	echo "size=+, type=07" | sfdisk -a --no-reread /dev/mmcblk?
	partx -u /dev/mmcblk?
	mkfs.exfat -n "Media" /dev/mmcblk?p3
	poweroff
fi

This script is run on every boot and checks for the presence of the media partition. If it can’t find it runs sfdisk to create it, formats it to ExFAT and powers off the system so that files can be placed on the new Media partition.

The player script

The media playback is handled by a single script which sets up the audio output, scans the media partition for playable files and runs VLC to play them back.

#!/bin/bash

# play.sh

set -e
shopt -s nullglob

audio_only=0

if [ -d /sys/class/graphics/fb0 ]
then
	card=$(basename /sys/class/graphics/fb0/device/drm/card?/)

	if [ $(</sys/class/graphics/fb0/device/drm/$card/$card-HDMI-A-1/status) = "connected" ]
	then
		echo "Using HDMI port 1"
		AUDIO_DEV="hdmi:CARD=vc4hdmi0,DEV=0"
	elif [ $(</sys/class/graphics/fb0/device/drm/$card/$card-HDMI-A-2/status) = "connected" ]
	then
		echo "Using HDMI port 2"
		AUDIO_DEV="hdmi:CARD=vc4hdmi1,DEV=0"
	else
		echo "Audio Only (No Connection)"
		AUDIO_DEV="default"
		amixer set Master 100%
		audio_only=1
	fi
else
	echo "Audio Only (No FBDev)"
	AUDIO_DEV="default"
	amixer set Master 100%
	audio_only=1
fi

if [ $audio_only != "0" ]
then
	files=(/mnt/*.{mp3,wav,flac,ogg})
else
	files=(/mnt/*.{mp4,mp3,mov,wav,flac,ogg})
fi

echo "Playlist: ${files[@]}"

if [ ${#files[@]} != "0" ]
then
	cvlc --vout drm_vout --drm-vout-source-modeset --aout alsa --alsa-audio-device $AUDIO_DEV --loop --random --no-video-title "${files[@]}"
fi

After setting some options for the script interpreter (set -e to abort execution if any command fails and shopt -s nullglob to ignore unexpanded shell globs) we check the sys filesystem if there is a screen connected to any of the HDMI outputs and select the appropriate audio output.
This ensures that no matter which HDMI output the projector is connected to we always play the audio through the right output. If there is nothing connected to either HDMI output we fall back to the “default” ALSA device (probably an external USB audio interface in that case), set it’s volume to 100% and set the operating mode to “audio only”.

If you’re in a situation where audio is always played through an external USB interface you can skip this step.

Lastly we take all the audio and/or video files (depending on if we’re in “audio only” mode or not) form the Media partition, assemble them into a playlist and pass that off to VLC to play back. You could use find to also make it take sub directories into account when listing the files, but by this point I had run out of energy and just wanted to get the thing over with.


Download

All scripts to set up your own media player are included in the attached mediaplayer.tar.xz file.

This same method could also be used to set up any other kind of minimal single function system of course, just by altering the installed packages and startup scripts.

If you have any feedback feel free to send me a message somewhere.

Love,
Kris ^-^


Addendum

I don’t like writing articles that feature specific commercial products or advertise them in any way, especially ones that are partially closed source like the Raspberry Pi and that are made by a company like the Raspberry Pi Foundation that has made various questionable decisions in the past.

I still wrote this up in the hopes that it would be useful to others.

Attachments