The original design requirements for the PiBox Media Center were simple.
Play video over wireless connections on HDMI interfaces.
The goal was to be able to watch videos in our trailer. In fact, we intended to watch videos on the side of our trailer. We call it Drive In Movie night. But before we could get there, I had to come up with a UI to slap on top of the PiBox Development Platform.
To play videos would be easy. There is the Raspberry Pi sponsored app omxplayer. But it just plays videos. It doesn’t select which videos to play or provide metadata about the videos, such as title and description. So I needed an front end to omxplayer. That’s what VideoFE does.
But how do you start VideoFE? I need a launcher system, something akin to an Android launcher but that works more like a Roku (because I like it better for this use case than Android apps). So I started with that: the PiBox Media Center launcher app.
The launcher is a very simple app. It reads app configuration files to know where to find icons and splash images, where the app binary lives and how to run it. It also needs to know how to draw icons, position them and highlight them.
In the original implementation of the launcher icons were required to be a specific size. Additionally the layout of icons was limited to 9 apps because only 9 cells were visible at any one time. At a monthly PiBox Meetup meeting someone brought up the question: is the launcher scalable? Well, no. It wasn’t.
So I looked at what was required to add some scaling options to the launcher. First, the icons needed to fill their cells no matter the screen size. This means app icons would both fill a big screen TV and squeeze into a 7″ touchscreen display. This requirement would also include scaling the highlight associated with the active icon.
Next, the number of cells needed to be adjustable. While I should add paging (perhaps the way the Roku does it) I opted instead focus on dealing with a properly displayed subset of icons. The main impetus for this change came from my need to add a kiosk mode to support the memorial to my dog Reba who passed away early in 2017. In kiosk mode I only needed two icons in a single row of cells, centered on the screen.
So this boils down to two requirements: scaling icons and setting the number of cells. Pretty straight forward. But think about this: once I know how many cells I have, how to I determine their size, which I need so I can scale icons to fit the display? The answer is pretty simple: get the screen dimensions early and make them available to all apps.
And this is where running on the Raspberry Pi comes in handy. The utility tvservice, provided in the Raspberry Pi firmware, provides a means of querying the system for display type and size. This is now done in the core platform boot process on firstboot. A configuration file is generated and can be queried using new libpibox functions.
- piboxLoadDisplayConfig() – loads the configuration file. Data is parsed from the system file but can only be retrieved using associated APIs.
- piboxGetDisplayWidth() – retrieves the width of the display.
- piboxGetDisplayHeight() – retrieves the height of the display.
- piboxGetDisplayType() – retrieves the display type. There are two types: LCD and HDMI.
- piboxGetDisplayClass() – Display class is just a short cut for common display sizes: PIBOX_DISPLAY_SMALL is <= 800 x 480, PIBOX_DISPLAY_BIG is > 1600 x 1200 and PIBOX_DISPLAY_NORMAL is everything in between.
Using the APIs allows me to change later from reading a static file to making the tvservice query on each call. Currently I prefer using the configuration file because I strip out only the relevant information. But I can switch to another mechanism (or port to another platform) without apps having to change how they get that information.
The launcher display is split in two. The right side shows a splash image, similar in style to the Roku UI. The left side displays icons for the various apps. The splash region width and height are set to fixed sizes depending on the display type. The remainder of the display space is allotted to the icon area.
The cell space is computed as
display width – splash width / columns
display height – splash height / rows
Each icon is read in using gdk_pixbuf and its dimensions retrieved.
image = gdk_pixbuf_new_from_file(path, NULL);
width = gdk_pixbuf_get_width(image);
height = gdk_pixbuf_get_height(image);
Scaling to keep the aspect ratio is handled next. It starts by getting the ratio of width to height for both the image and the cell.
ri = width / height;
rc = cwidth / cheight;
These are compared in order to compute the new image width and height.
if ( rc > ri )
swidth = width * cheight/height;
sheight = cheight;
swidth = cwidth;
sheight = height * cwidth/width;
And finally the image is scaled to its proper size for fitting into the cell. This generates a new image buffer.
newimage = gdk_pixbuf_scale_simple(image, swidth, sheight, GDK_INTERP_BILINEAR);
Next, the new image needs to be positioned in the cell. Note that the cell width and height was fudged (not shown here) so that those dimensions are slightly less than the real cell size, allowing a little empty space between icons. So centering in the cell is necessary.
offset_x = (req.width – swidth) / 2.0;
offset_y = (req.height – sheight) / 2.0;
At last we can use the gdk_pixbuf as the source for a Cairo surface to draw the icon.
gdk_cairo_set_source_pixbuf(cr, newimage, offset_x, offset_y);
One more thing to do: drop the references to the images used here to avoid leakage.
This scaling and positioning process is also applied to the splash image and the background highlight that is drawn beneath the active icon.
Implementation of these two simple changes to the launcher ended up providing all the PiBox library functions and techniques I needed to add scaling capabilities to any app. It’s already been applied to PiPics and will eventually be applied to all apps.