Visionary Pi Digital Signage

So over the past few days, I’ve looked into how to setup digital signage for a kiosk at my local Chamber of Commerce. I reviewed a few items in terms of hardware, which then led me to look into digital signage services and open source projects. Services are roughly $10 per month and upwards, while some offer a free version that’s fairly limited and unusable. Open source products seem to focus on a simple rotation of images, and not much in the way of managing anything via web browser. A few solutions were found to tie in smart TV’s like a Roku or Google Chromecast witch could display images as a slideshow from your Google Pictures account.

From the research involved, I went ahead and started on a small open-source solution that would run on a Raspberry Pi stuck on the back of a television with Velcro strips. I was able to setup Ubuntu desktop and have it start-up a Google Chromium web browser in kiosk mode pointing to a website to rotate the images. From there, I progressed and setup Apache, PHP, and MariaDB for a full LAMP stack via Docker containers, and using phpMyAdmin to manage the database.

I created a Logo, a project name, and started working on the website itself using React, Redux, and RTK Query on the Front-End along with MUI user controls. I had setup a template and a few routes, and got as far as uploading files to the server.

I’m in a bit of a rush, as I want to demonstrate to the Chamber what I can setup for them if they like what they see, and get some feedback on what they are looking for in a digital signage system. If they like it, it may lead into other businesses that may want a similar solution.

So what exactly is the plan? Let’s just speculate on what we could do

  • Media
    • Upload files – done
    • List files
    • Search files
    • Rename files
    • Sort files
    • Scan for new files added manually
    • Save file names in the database
    • Filter files by file type
    • Detect durations of video, audio, and animated GIFs
    • Delete files
  • Campaigns
    • Manage a list of campaigns
    • Choose what media fits into a campaign
    • Choose if media appears in a specific order, or random
    • Choose active periods such as holidays, or days leading up to public events
    • Allow campaigns to have a draft version, so displays already showing the published campaigns do not see the changes until published.
  • Displays
    • Setup a default display
    • Assign multiple campaigns to the display
    • Rename a display
    • Show a preview of what the display will show
    • Bonus
      • Register multiple displays
      • Specify display business
      • Specify display location
      • Specify display type (1080p, HDTV, tablet)
      • Setup display registration process
  • Digital Signage
    • A web page for web browsers to point to and run the various campaigns
  • Bonus
    • User management
      • Setup access controls to manage media, campaigns, displays
      • JWT Authorization with refresh keys
      • Restrict access to prevent deleting content or seeing content
    • Enterprise management
      • Look into how multiple clients can log into a centralized system to manage their own displays, campaigns, media
    • Community
      • Allow clients to publicly share images, trivia facts, etc.
      • Allow local businesses to advertise each others events

Fun stuff. However, we are on a time crunch. Let’s figure out what we should do for a proof of concept.

  • Media
    • List uploaded files
    • Paginate – maybe Sort/Search
    • Delete
    • Rename
  • Campaign
    • List campaigns
    • Add/Edit/Delete
    • Assign photos
    • Assign display duration
    • Enable/Disable
    • Specify date range, or none
  • Display
    • Add/Rename/Delete
    • Assign campaigns
    • Display special URL to identify display
    • Link to Digital Signage page
  • Digital Signage
    • Display the content

Ok, sounds good. Let’s run with the media first since we started there yesterday. Let’s list all files that have been uploaded. Since the majority of our files are visual, except maybe a few audio files for background music, I want to display a more graphically rich list of content. I believe the way to go is to use the MUI Image List component.

Before we go to displaying the images, let’s create an API that can scan the folder and update the database with what’s there. Well this is odd. I can SSH into the pi, but I can’t ping it. The web browser times out. If I go to the Pi itself, it’s able to view localhost. Docker containers are running fine. Maybe it’s setup not to pong? Let’s just reboot the pi itself. That did the trick!

We can use a recursive directory iterator to list out all files.

function find_files_and_sizes($path)
{
    if (!is_dir($path)) {
        throw new InvalidArgumentException("$path is not a valid directory");
    }
    $files = [];
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
    );
    foreach ($iterator as $file) {
        if ($file->isFile()) {
            $files[] = [
                'path' => $file->getPathname(),
                'name' => $file->getFilename(),
                'extension' => $file->getExtension(),
                'size' => $file->getSize(),
                'created' => $file->getCTime(),
            ];
        }
    }
    return $files;
}
    "files": [
        {
            "path": "\/var\/www\/html\/uploads\/Screenshot_2024-10-25_at_10.18.09___PM.png",
            "name": "Screenshot_2024-10-25_at_10.18.09___PM.png",
            "extension": "png",
            "size": 658285,
            "created": 1729921686
        },
        {
            "path": "\/var\/www\/html\/uploads\/Screenshot_2024-10-25_at_10.18.56__8239_PM.png",
            "name": "Screenshot_2024-10-25_at_10.18.56__8239_PM.png",
            "extension": "png",
            "size": 647667,
            "created": 1729921033
        }
    ]
}

That gets us pretty far. I can use the path as a unique key to update database entries. Before updating the database, I can mark all records as missing, and then restore them as existing as I iterate through each file.

How about dimensions and durations? Can I read that with native PHP, or do I need a library or parse headers from a file?

Reading File Metadata

PHP has a built-in function to read image dimensions.

list($width, $height, $type, $attr) = getimagesize($pathname);

Reading video dimensions & duration, and audio durations is done via ffmpeg on the command line. I’ve updated my dockerfile to install the package. Here is a script that can parse the output for PHP.

function get_audio_video_info($path)
{
    $info = [];
    $output = shell_exec("ffmpeg -i " . escapeshellarg($path) . " 2>&1");
    if (preg_match("/Duration: (\d+:\d+:\d+\.\d+)/", $output, $matches)) {
        $info['duration'] = $matches[1];
    }
    if (preg_match("/Audio: ([^ ]+)[^:]+?(\d+) Hz, (mono|stereo)/", $output, $matches)) {
        $info['audio_format'] = $matches[1];
        $info['audio_hz'] = $matches[2];
        $info['audio_channels'] = $matches[3];
    }

    if (preg_match("/Video: ([^, ]+),? [^:]+, (\d{2,4})x(\d{2,4}), /", $output, $matches)) {
        $info['video_format'] = $matches[1];
        $info['width'] = $matches[2];
        $info['height'] = $matches[3];
    }
    return $info;
}

I’ve also created a SQL script to setup a few tables to normalize the data that was found. I’ve now got a script that scans the folder and sub folder for new/modified/renamed/deleted files. If someone deletes a file and puts it back, the metadata is still retained in the database.

At this point, I can go ahead and write an api to expose what has been stored in the database.

Ehh… I’ve been buried in code. I’ve mainly been working on image previews. Given that this is a media-rich application, I wanted to show the assets. Rather than loading full-sized images and entire videos, I decided to resize images, and pull out video frames. I used FFmpeg to grab the video, which is also capable of visualizing audio as well, so I generated audio previews too.

The GD library was used for resizing images. It’s not on the php docker image, so I had to grab the package. Not only that, but there were no installation candidates for php gd, so I had to compile the code as well. In the end – I can resize my images. Currently, I’m only resizing jpeg, png, and gif images. Animated GIF’s lose their animation in the thumbnails.

FFmpeg seems to think that Jpegs and SVG files have a duration of 40ms. I’ll have to visit this and figure out what’s going on here. The nice thing is that you can point at a specific point in the video and grab the image. I’ve created pages to generate previews of audio, video, and images. Each one allows you to pass the width/height as a query string parameter. Audio lets you pass in the color. Video lets you pass in seconds. If the height is -1 for image or video, then the height is calculated based on the existing aspect ratio. I have three other files that will update the database entry for the given file. This way, previews can be created out-of-process.

I suppose if an MP3 file had exif data with a thumbnail, I could probably use that image instead. I just haven’t dug too deep into all possibilities. I think I’m at a point where I’ve got a list of images that’s good enough to present.

…a few hours later.

I have a slide show iterating through all of the media. By default, it displays each image for 5 seconds. Audio and Video are allowed to play through to completion before moving to the next media file. For now, I’ve muted the video, and deleted the audio files, as it was getting annoying listening to the same stuff again and again. I even hid the mouse cursor, as it was sitting on the screen after I had bumped the trackball, and wouldn’t go away.

Part of the setup for the front page was to reconfigure the react-router-dom to have a template show up only for the administration of assets. Otherwise the main page is a blank page.

I ended up deleting all of my test files and uploading a bunch of stuff from the clients website as slides. I noticed that a few of the images were webP format. I didn’t have anything installed to resize them, so I went back to the dockerfile and installed libwebp-dev and added a –with-webp flag when configuring the gd extension for php.

I think I’ve got just about everything ready, as a bare bones system to demonstrate to the client. I can simply plug the Raspberry Pi to the television and let it boot up to automatically show the digital signage.

From there, I can press [ALT]+[F4] on the keyboard and open a browser to show her how the content is managed in the back-end. Just for good measure, I put two icons on the desktop – one for the kiosk mode, the other to manage the content.

I wanted to get more done, but quite a bit slowed me down along the way. Installing and compiling libraries to create preview thumbnails and pull out metadata about audio/video duration is practically necessary.

I was concerned about videos playing on the Raspberry Pi. From what I see, they start up fast and play smooth. I may set this up on a Pi Zero to see if its also capable of playing the content. A Pi Zero is about a third of the cost of a Pi 5, and I think getting the software running on that version would help the open-source community more. I think I also need to look at the docker setup sometime in terms of making things lightweight.

So what’s left… nothing for now. I just need to show up and plug in. I don’t even need an internet connection since it’s all local. I’ve tagged the source code as v1.0.0. I’ve got to gather my notes about the other digital signage services that I found in case they would prefer a cloud based service.

Thinks I would like to have gotten to:

  • Rename files
  • Physically delete the file when it’s removed from the database
  • Create preview images as a cron job
  • Setup caching to refresh when images are uploaded/deleted
  • Upload multiple images
  • Drag images onto the page to upload
  • Editing a playlist (campaign) to choose the order and duration of each media item
  • Setup a default background image, like a slide deck
  • Display content from various URL’s (Calendar, YouTube, Facebook, Twitter/X)
  • Display a Google slide show, and display each slide
  • Setup background music/rotation
    • Setup a list for trivia, fun facts, quotes, etc.
    • Request for next slide, rather than loading once and looping
      • add/delete images do not appear unless manually refreshed/rebooted

    It’s a bare bones slide show system for now, but the groundwork has been laid out.

    In other news, I saw a prior client reached out to me last week. Perhaps a big project may be in store.

    My case for the Raspberry Pi 5 arrived as well. Unfortunately, there are a few things going on where I can’t use it. The main one is the PCIe adapter can’t be plugged in because the M.2 hat is already using that for the AI chip. The adapter is primarily for a Gigabit network interface. I should have looked more into what was going on with all the gizmos. The only reason I would need it, is if was sitting next to the optical network terminal with the routers. Because the M.2 hat is screwed into the Pi, I’m unable to secure the Pi into the case. I’d be relying on the other cards that plug into it in order to hold it in place. Ah well, the Pi is fine where it is at for now.

    Discover more from Lewis Moten

    Subscribe now to keep reading and get access to the full archive.

    Continue reading