0123456789 minute read
A Plex powered, Netflix inspired screensaver.
Home Cinema#android#plex
Will Beeching
---
title: A Plex powered, Netflix inspired screensaver — Machine-readable
source_page: https://willbeeching.com/a-plex-powered-netflix-inspired-screensaver/
canonical: https://willbeeching.com/a-plex-powered-netflix-inspired-screensaver/
format: text/markdown
last_updated: 2025-12-30
description: An exploration of creating an elegant Android TV screensaver that dynamically pulls artwork from Plex, optimizing aesthetic quality and user experience.
---
**Quick links:** [Human page](https://willbeeching.com/a-plex-powered-netflix-inspired-screensaver/) · [Home](https://willbeeching.com) · [Blog](https://willbeeching.com/blog)
---
# A Plex powered, Netflix inspired screensaver — Machine Version
**Categories:** Home Cinema
**Tags:** android, plex
---
## Background Many moons ago, I [wrote about making a screensaver](https://willbeeching.com/a-netflix-style-screensaver-for-plex-android-tv/) for Android TV that was inspired by the Netflix screensaver. The setup was fairly hacky, a small web server hosting a curated set of movie and TV artwork, and an Android app that simply pointed the screensaver at a URL. It worked for a while but new films and shows meant constantly updating the CMS, and eventually it fell out of date and got abandoned. The obvious fix was to pull directly from my Plex library so it always reflected what I actually owned. I just never got around to it. Until recently. The goal was simple: no CMS, no manual curation, no stale artwork. Just authenticate with Plex, pull whatever I actually own, and display it as a native Android TV screensaver that doesn’t look like a demo loop. ## Getting beautiful artwork I actually thought this part would be pretty straightforward. I initially started using TMDB and TVDB as a source for the artwork. TMDB backdrops are fine for apps, but unreliable for ambient use, too much text, inconsistent language, and a lot of marketing-led imagery.
### Logo Artwork I realised that Plex recently released their new experience on mobile which had fantastic high res logo artwork. When Plex scans your media library it auto fetches metadata including these logo artwork. By default these aren’t exposed through the standard API, but if you include the following to the request. includeImages=1&includeExtras=1 Plex returns XML with `<Image>` child elements like this: <Video> <Image type="clearLogo" url="/library/metadata/12345/clearLogo/67890" /> </Video> I found these to be pretty good, they’re usually white or light-coloured, which makes them easy to place cleanly over darker backgrounds. In case Plex hasn’t got a clear logo for a media item I set a fallback to [Fanart.tv](http://fanart.tv/) or TMDB if it still fails.
### Backdrop artwork This was the tricky one, as mentioned I started off using TMDB/TVDB. However I was getting pretty poor responses. I looked into figuring out where Plex got it’s backdrop artwork from, but couldn’t seem to find anything in the API like the clearLogo. It’s also **not** the one that you manually select in the UI either. After digging around for alternatives, I landed on [Fanart.tv](http://fanart.tv/), which has a huge amount of high-quality backdrop artwork and crucially, is mostly logo-free. We then filter these based on the following: - Text free first, images with lang="" - Highest resolution - Community likes, most popular rise to the top
```text
/**
* Get best background from a list of Fanart images
* Prefers: 1) Highest resolution, 2) Text-free (lang=""), 3) Most likes
*/
private fun selectBestBackground(backgrounds: List<FanartImage>?): FanartImage? {
if (backgrounds.isNullOrEmpty()) return null
// Prefer text-free backgrounds (empty lang)
val textFree = backgrounds.filter { it.isTextFree }
val candidates = if (textFree.isNotEmpty()) textFree else backgrounds.filter { it.lang == "en" }.ifEmpty { backgrounds }
// Sort by resolution (highest first), then by likes
val sorted = candidates.sortedWith(
compareByDescending<FanartImage> { it.widthInt }
.thenByDescending { it.likesInt }
)
val best = sorted.firstOrNull()
if (best != null) {
val langType = if (best.isTextFree) "text-free" else "lang=${best.lang}"
Log.d(TAG, "Selected $langType background: ${best.resolution}, ${best.likesInt} likes")
}
return best
}
```
If nothing can be found it’ll then fall back to Plex, or TMDB/TVDB. I initially started off wanting full 2160p artwork, however Fanart does not seem to expose 4K artwork through the api. Hopefully in the future they add this, but 1080p still looks fantastic as most 4K tvs will upscale.
## Making it look great For the full Netflix inspired effect, I wanted to avoid just darkening the corners of the image where I’d display the film/show logo. I initially wanted to use a coloured blur radiating from the left corner, however after lots of trial and error. I figured out that Android TV on my NVIDIA Shield Pro doesn’t support blurring. The next best thing was to add a radial gradient positioned in the corner using the prominent colour from that area of the image. Plex have managed this in their app by saving prominent colours in the image file in the XML for each media item. However as I was grabbing these media items that could be different each time, this wouldn’t work. So as each image loads the app works out the prominent colours from that section of the image and applies them to the gradient creating a more seamless effect behind the logo artwork.
```text
private fun updateGradientsWithImageColor(targetView: ImageView) {
val drawable = targetView.drawable ?: return
scope.launch(Dispatchers.Default) {
try {
// Get bitmap from drawable
val originalBitmap = if (drawable is BitmapDrawable) {
drawable.bitmap
} else {
drawable.toBitmap()
}
// Convert hardware bitmap to software bitmap if needed
val softwareBitmap = if (originalBitmap.config == android.graphics.Bitmap.Config.HARDWARE) {
originalBitmap.copy(android.graphics.Bitmap.Config.ARGB_8888, false)
} else {
originalBitmap
}
val width = softwareBitmap.width
val height = softwareBitmap.height
// Define corner regions (bottom 40% height, left/right 40% width)
val cornerWidth = (width * 0.4f).toInt()
val cornerHeight = (height * 0.4f).toInt()
val bottomY = height - cornerHeight
// Extract bottom-left corner
val bottomLeftBitmap = android.graphics.Bitmap.createBitmap(
softwareBitmap,
0,
bottomY,
cornerWidth,
cornerHeight
)
// Extract bottom-right corner
val bottomRightBitmap = android.graphics.Bitmap.createBitmap(
softwareBitmap,
width - cornerWidth,
bottomY,
cornerWidth,
cornerHeight
)
```
This is handled using a simple priority system: - Dark Muted (first choice) – Subdued, professional colours - Dark Vibrant (fallback) – Richer, more saturated colours - Muted (second fallback) – Any muted colour - Pure Black (last resort) – If no colours found This priority ensures the gradients are dark enough to maintain contrast with the white logo text while still having colour personality from the artwork. We then smoothly transition the gradient as each new artwork loads.
## Adding movement and depth Finally adding some slight movement to the image as it’s on screen to reinforce the separation between the logo and the backdrop I added a slight Ken Burns effect that slowly pans the image across the screen.
```text
private fun applyKenBurnsEffect(targetView: ImageView) {
// Random horizontal direction: pan left or right
val panDistance = 60f
val startTranslateX = if (Math.random() < 0.5) -panDistance else panDistance
val endTranslateX = -startTranslateX
// Zoom in more for a more obvious pan effect
val scale = 1.15f
// Cancel any existing animation to prevent glitches
targetView.animate().cancel()
targetView.clearAnimation()
// Reset position and apply zoom
targetView.scaleX = scale
targetView.scaleY = scale
targetView.translationY = 0f
targetView.translationX = startTranslateX
// Animate horizontal pan - much longer duration to continue throughout entire display
// Make it 2x the rotation interval to ensure smooth continuous motion
val animationDuration = ROTATION_INTERVAL_MS * 2
targetView.animate()
.translationX(endTranslateX)
.setDuration(animationDuration.toLong())
.setInterpolator(android.view.animation.LinearInterpolator())
.withEndAction(null) // Clear any end actions
.start()
}
```
Then when transitioning between each image we load in the new image behind the current and crossfade between the two to avoid having fade to black transition.
```text
companion object {
private const val TAG = "ScreensaverController"
private const val ROTATION_INTERVAL_MS = 10000L // 10 seconds total per slide
private const val CROSSFADE_DURATION_MS = 2000L // 2 seconds for backdrop transitions
private const val LOGO_FADE_IN_DELAY_MS = 500L // Delay after backdrop before logo appears
private const val LOGO_FADE_DURATION_MS = 1000L // Logo fade in/out duration
private const val LOGO_DISPLAY_TIME_MS = 15000L // How long logo stays visible
}
```
## Want to check it out? The app itself is fully working. The remaining work is around distribution, specifically allowing users to supply their own API keys without forcing them to type long strings using a TV remote. Once that’s solved, I’ll publish the APK here. If there’s interest, I’ll also put it on the Play Store for a small fee so people don’t need to sideload it. ## v0.1.0 Alpha After the huge amount of love that this received I’m putting out an alpha version for people to test across devices. This is a very early release so do expect bugs and log any [issues](https://github.com/willbeeching/flix/issues) in GitHub and I’ll endeavour to get them fixed. So far this has only been tested on an NVIDIA Shield. You’ll need to side load it onto your device, you can do this by downloading the app from the releases page on the repo. Copy it to a USB stick and then you should be able to install it to your Android TV device. [Download v0.1.0 here](https://github.com/willbeeching/flix/releases). Also if there’s anyone who would like to contribute to make this app better, please do submit a PR.
There's more like this.
Keep reading.
Will Beeching
Coming soon
Security
Automatic gate entry using licence-plate recognition and UniFi Access.
Coming soon
Coming soon
Will Beeching
Coming soon
Climates
An adaptive climate system for year-round comfort.
Coming soon
Coming soon