
Loop Videos on Dual Displays Using a Raspberry Pi 4b

Earlier this year, I was tasked with creating digital signage for a Jeep dealership. The client wanted to mount eight TVs — four on either side of their vehicle showroom — and have them play a collection of ~400 lifestyle videos asynchronously. Right away, the thought of using Raspberry Pis came to mind. The Pi 4b, with its dual-monitor capability, seemed like the obvious choice for this project. I bought four of them, intending to drive two TVs with each.
Solid plan, as you'll no doubt agree. In fact, the plan was so solid, I was positive someone else had done it already. But no! To my surprise, no one had. Some solutions existed, but none supported two displays. So, I took on the challenge myself. In the end, I was able to turn the Pi 4b into a totally hassle-free, plug-and-play video looping device. Read on to learn how you can do the same.
What You'll Need
- Raspberry Pi 4b
- Adequate cooling (heatsinks + 40mm fan recommended)
- Raspberry Pi OS Lite, 2020-02-13 or newer
- USB flash drive
Notes
- My project required video alone. Audio was not a consideration. Sound does work, but keep in mind that only one of the micro-HDMI ports on the Pi 4b carries audio.
- Omxplayer is lightweight and fast, but not as robust as VLC and other media players. Because omxplayer can only play a handful of formats, I purposely designed my script to ignore files with extensions other than .mp4. For best results, use 1080p videos with h.264 encoding in mp4 format.
- The Pi 4b is hardware-limited to 1080p @ 60hz per display or 2160p (4k) @ 30hz per display. Omxplayer will skip videos that exceed these specifications.
Setting Up USB Auto-Mounting
A USB stick is the most logical place to store your video files. That way, you or your client can easily add and replace videos in the future. Raspberry Pi OS doesn't automatically mount USB storage media by default, so our first step is to enable this behavior.
There are several ways to accomplish this, especially if you know the device's UUID. But since we can't be sure whether the same USB stick will always be used with your Pi, we'll employ a more comprehensive method.
The instructions provided by pauliucxz on StackExchange are perfect for our needs. With a udev rule, systemd service, and mount script, we can ensure that any attached USB stick is not just mounted at boot, but also assigned a predictable mount point.
Dependencies
Install the required pmount package using sudo apt-get install pmount
.
Udev Rule
Create the file /etc/udev/rules.d/usbstick.rules
with the following content:
ACTION=="add", KERNEL=="sd[a-z][0-9]", TAG+="systemd", ENV{SYSTEMD_WANTS}="usbstick-handler@%k"
Systemd Service
Create the file /lib/systemd/system/usbstick-handler@.service
with the following content:
[Unit] Description=Mount USB sticks BindsTo=dev-%i.device After=dev-%i.device [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/bin/cpmount /dev/%I ExecStop=/usr/bin/pumount /dev/%I
Mount Script
Create the file /usr/local/bin/cpmount
with the following content:
#!/bin/bash if mountpoint -q /media/usb1 ; then if mountpoint -q /media/usb2 ; then if mountpoint -q /media/usb3 ; then if mountpoint -q /media/usb4 ; then echo "No mountpoints available!" else /usr/bin/pmount --umask 000 --noatime -w --sync $1 usb4 fi else /usr/bin/pmount --umask 000 --noatime -w --sync $1 usb3 fi else /usr/bin/pmount --umask 000 --noatime -w --sync $1 usb2 fi else /usr/bin/pmount --umask 000 --noatime -w --sync $1 usb1 fi
Make the file executable for the root user using sudo chmod u+x /usr/local/bin/cpmount
, then insert your USB stick and reboot. If you did everything correctly, your USB stick should now be mounted at /media/usb1
automatically at system startup.
Installing Playback Script
Now that we have a consistent location for our videos, we can install the script responsible for playing them. I call the following script “Video4Pi.” Based loosely on a script written by AliOs from Key to Smart, my script adds useful features and the ability to drive two separate displays.
Dependencies
Install the required omxplayer package using sudo apt-get install omxplayer
.
Parent Script
Create the file ~/video4pi.sh
with the following content:
#!/bin/bash # Detect number of connected displays read -r dispcount _ < <(tvservice --list) # Mount point of USB device videos="/media/usb1" # Default variables (overriden by config.txt file on USB device if present) run="true" sync="true" shuffle="true" between="0" screen1_options="--display=2" screen2_options="--display=7 --no-keys" # Check for video files on USB device if ls $videos/*.mp4 &>/dev/null ; then filecount=$(ls $videos/*.mp4 | wc -l) else filecount=0 fi # Get custom configuration file from USB device if [[ -f $videos/config.txt ]]; then custom="true" source $videos/config.txt fi # Provision for invalid 'between' values case $between in ''|*[!0-9]*) between=0 ;; *) ;; esac # Disable cursor for clean video transitions setterm -cursor off; # Intro message echo -e "--------------------------------------------------------------------------------\n" echo -e "\nThis program is intended to play videos with h.264 encoding in .mp4 format only!\nGPU hardware decoding on this device is limited to 1920x1080 @ 60hz. Videos with\ngreater resolution or frame rate will be skipped." echo -e "\nPress Ctrl+C to stop the program now." echo -e "\n--------------------------------------------------------------------------------" if $custom = "true" ; then echo -e "\nConfiguration override file found. Proceeding with the following settings:" fi echo -e "\nSync is $sync | Shuffle is $shuffle | Delay between videos is $between seconds" echo -e -n "\nFound $filecount compatible videos. " # Begin perpetual loop if $run = "true" ; then if [ "$filecount" -gt "0" ] ; then echo -n "Starting in " for i in {5..1}; do echo -n "$i... " && sleep 1; done while true; do if $shuffle = "true" ; then playlist1=$(for entry in $videos/*.mp4 ; do echo "$entry" ; done | sort -R) playlist2=$(for entry in $videos/*.mp4 ; do echo "$entry" ; done | sort -R) else playlist1=$(for entry in $videos/*.mp4 ; do echo "$entry" ; done) playlist2=$(for entry in $videos/*.mp4 ; do echo "$entry" ; done) fi if [ "$dispcount" -gt "1" ] ; then if $sync = "true" ; then if ps ax | grep -v grep | grep omxplayer > /dev/null ; then sleep 1; else for entry in $playlist1 ; do clear omxplayer $screen1_options "$entry" > /dev/null & omxplayer $screen2_options "$entry" > /dev/null sleep $between done fi else if ps ax | grep -v grep | grep "screen1.sh" > /dev/null ; then sleep 1; else for entry in $playlist1 ; do clear source ./screen1.sh sleep $between done fi & if ps ax | grep -v grep | grep "screen2.sh" > /dev/null ; then sleep 1; else for entry in $playlist2 ; do clear source ./screen2.sh sleep $between done fi fi else if ps ax | grep -v grep | grep omxplayer > /dev/null ; then sleep 1; else for entry in $playlist1 ; do clear omxplayer $screen1_options "$entry" > /dev/null sleep $between done fi fi done fi else echo "Run is set to false; exiting." fi
Make the file executable using chmod +x ~/video4pi.sh
.
Child Scripts
Next, we're going to create two child scripts — one for each display. Their purpose is to track each display's independent playback status and signal the parent script to load a new video once the current video is finished.
Create the file ~/screen1.sh
with the following content:
#!/bin/bash omxplayer $screen1_options "$entry" > /dev/null
Create the file ~/screen2.sh
with the following content:
#!/bin/bash omxplayer $screen2_options "$entry" > /dev/null
Make both files executable using chmod +x ~/screen1.sh ~/screen2.sh
.
Config File
The Video4Pi script is highly configurable. All settings can be managed via a config file, which we'll now place on the USB stick containing your videos. This will make it easy for you to change settings at any time without dislocating the Pi.
Create the file /media/usb1/config.txt
with the following content:
# This file should be stored on your USB device. Edit the variables below to # customize the Video4Pi script's behavior for your needs. # ------------------------------------------------------------------------------ # Setting to false prevents playback from starting. Useful for debugging. run="true" # When set to true, displays are synchronized. When set to false, displays # act independently. Note: if shuffle is false, displays will be identical # regardless of the sync setting. sync="false" # When true, videos are played at random. When false, videos are selected by # filename in alphanumeric order. shuffle="true" # Sets a delay period between videos (in seconds). between="0" # ------------------------------------------------------------------------------ # CAUTION: Settings below this line should not be changed unless you know # exactly what you're doing! Faulty values may cause the script to malfunction. screen1_options="--display=2" # omxplayer arguments for 1st display screen2_options="--display=7 --no-keys" # omxplayer arguments for 2nd display
Edit the variables in this file to suit your needs. As it explains, you can use the “sync” and “shuffle” variables to decide whether your two displays are synchronized or independent. If you want more advanced control over playback on either display, you can append additional omxplayer arguments to the “screen1_options” and “screen2_options” variables.
Increasing GPU Memory
After that last step, you probably tried to test the parent script... only to be told by omxplayer to “have a nice day!” That's because there isn't enough memory allocated to the GPU to play two videos at once. Let's fix that.
Bring up the raspi-config utility using sudo raspi-config
.

Beginning from the main menu, select “Advanced Options” followed by “Memory Split.” When prompted, enter 512 as the new value and select “Ok” to confirm the change. This will allocate 512 MB of system memory to the GPU.
This is the maximum amount the OS will allow, so don't bother entering a higher number. It will just reset to default. On the other hand, you're welcome to try allocating less memory, but I don't advise it. Some videos are more demanding of the GPU than others. Giving it all the memory you can spare guarantees a seamless experience.

When you're finished with that, reboot. Now, try running the parent script again. With more available memory, the script should work as expected and you should get output on both displays. We're almost done! Use Ctrl+C to exit the script before moving on to the final steps.
Starting Playback Script at Boot
Most likely, you want videos to start playing automatically when the Pi is turned on. To make that happen, all you need to do is make ~/video4pi.sh
run at boot. Any way you choose to accomplish this is fine.
A quick and dirty method is to add the script to the default user's bash profile. This can be done with a single command: echo "source ./video4pi.sh" > ~/.bash_profile
.
With that in place, Video4Pi should now start by itself each time the system boots. Feel free to test this now if you like. But use Ctrl+C during the script's countdown to cancel it for the time being, as there's one more thing we should do before wrapping this up.
Enabling OverlayFS
You can't rely on your client to log in and initiate a clean shutdown when they want to turn off the Pi. Plus, there's always the danger of a power outage. Regardless of how they happen, unclean shutdowns cause filesystem corruption that can cripple a Raspberry Pi in a matter of weeks.
Luckily, we can avoid that outcome by making the Pi's filesystem effectively read-only. Now an included option in Raspberry Pi OS, OverlayFS makes it so that all writes to the root filesystem are stored in memory instead of on the SD card. When we're through, you'll be able to safely unplug your Pi just like you would a desk lamp or kitchen appliance. It won't need to be shut down properly like a PC.
Launch raspi-config again using sudo raspi-config
. Just like before, select “Advanced Options” from the main menu. From the advanced menu, select “Overlay FS” and answer yes to all of the prompts. When asked if you'd like to reboot, do so.

With OverlayFS enabled, you can count on your Raspberry Pi to work reliably no matter what. Best of all, it's completely reversible. If you want to write changes to the disk later on, simply use raspi-config to disable OverlayFS, returning the root filesystem to normal.
That's it! Your Raspberry Pi 4b is now ready for use as a dedicated dual-output video looping device.