Also by me: Pixelfin · JellyTag-Plus · TaskGrid
MediaWall is a way to take advantage of an old iPad, a Raspberry Pi with a display, or any spare screen by turning it into a live window into your Jellyfin and Navidrome servers. It can show what you and your users are currently watching or listening to, cycle through artwork from your libraries as a screensaver, or display favorite media as wallpapers. It also supports custom sounds, layouts, and fallback screens. Basically, it's a fun little project that's an overengineered way to view active sessions and make your media library visible instead of leaving all that artwork buried in storage.
MediaWall is under active development. There may be breaking changes, which will be highlighted in every release.
Like Pixelfin, this project is vibecoded with Codex. It's built with security in mind, but because it's a vibecoded self-hosted app, I can't promise it's hardened for hostile public exposure. Prioritize running it locally or behind access controls you trust.
In line with my ongoing obsession with the images and artwork in Jellyfin, as seen through my other project, Pixelfin, I wanted to combine my appreciation for the Jellyfin Android TV screensaver with the fact that I also like being able to glance over and see what people are currently watching or listening to on my Jellyfin and Navidrome servers.
Then I realized I had an old iPad laying around doing absolutely nothing. I wanted something that would work on that, or on something like a Raspberry Pi connected to a display. MediaWall was born.
MediaWall is a display app for Jellyfin and Navidrome built around three main features, and it's meant to work well on an old iPad, a Raspberry Pi connected to a monitor, or really any device with a browser. Each display space also has a phone-friendly remote at the same route with -remote appended, so you can control the display without walking over to it.
The first, and most prominent, is Now Playing. MediaWall shows what's currently being watched or listened to across your Jellyfin and Navidrome servers, along with artwork, user information, media details, and optional sound notifications when sessions start or end.
The sound system is customizable too. You can use one global sound, assign custom sounds to individual users, and control when sounds should or shouldn't play. This is especially useful with Navidrome or Jellyfin music libraries, where you probably don't want a notification every time the next song starts.
MediaWall can also react to configured Jellyfin collections by using collection-specific sounds and transition images when matching media starts playing. Collection matches are indexed during the existing Jellyfin library scan, persisted to disk, and reused without querying collection membership during playback. Items in several matching collections use the first alphabetical collection's sound and show all relevant collection images in alphabetical order.
The second feature is Screensaver mode. This is heavily inspired by the Jellyfin Android TV screensaver and cycles through artwork from your Jellyfin libraries, with additional options for controlling what appears and how it's displayed. The idea is to turn an otherwise unused screen into a constantly changing showcase for the artwork already sitting in your media collection. I know many of you have terabytes of media, but most of the time it's all just data sitting there. MediaWall gives you a way to passively browse your libraries and actually see more of it.
Third is Wallpaper mode. If MediaWall lands on something you particularly like, you can pause on that media item and use it as a static wallpaper. You can also choose favorites and have MediaWall cycle through those instead, essentially creating your own curated rotation of artwork.
While nothing is playing, MediaWall can cycle through your library artwork in Screensaver mode, show the bundled MediaWall screensaver or one with your own custom logo, or hand the display over to a configured Immich Kiosk setup. That way, even between playback sessions, the screen can keep working as a digital photo frame.
So depending on how you use it, MediaWall can be a live window into your Jellyfin and Navidrome servers, a Jellyfin-powered digital art display, or basically a very overengineered way to give an old iPad, Raspberry Pi, or spare screen something useful to do.
Is this necessary? No!
Is it a kind of fun excuse to use more electricity and tinker with something? Yes :)
Put MediaWall on an iPad, tablet, TV, or Raspberry Pi display in a shared room and use it as a live window into your media server.
If someone is watching something on Jellyfin or listening to music through Navidrome, MediaWall can automatically show active sessions with backdrop artwork, logos, playback information, source icons, and, if you want, the name or avatar of the person using it.
So instead of asking, "What are we listening to?" or checking Jellyfin manually, you can just glance at the display.
If you run Jellyfin for multiple people, MediaWall can also act as a simple visual dashboard for your server. Set it to watch all Jellyfin users, and the display will automatically show all active sessions.
It's an easy way to make activity on your server feel a little more visible and alive without opening an admin dashboard or staring at a list of sessions.
MediaWall doesn't have to show playback activity at all.
You can put it on a desk, shelf, wall-mounted tablet, or Raspberry Pi-connected display and use it as a rotating screensaver for the artwork in your Jellyfin library. I know that many of you have terabytes of media, but it's all just data. MediaWall allows its viewers to passively browse your libraries.
If you don't want it pulling from everything, open the grid and favorite the artwork you actually want to see, or only allow specific libraries. MediaWall can then rotate through only those favorites or libraries, essentially turning your media collection into a curated digital art display.
And if one image looks especially good, just pause the slideshow and leave it there as a clean static wallpaper.
That's really the idea behind MediaWall: it can be a Now Playing display, a homelab dashboard, a screensaver, a wallpaper, or some combination of all of them depending on where you put it.
- Shows active playback sessions from Jellyfin, Navidrome, or both.
- Supports multiple MediaWall users per space, including Jellyfin
Allusers. - Can show a configurable user-intro transition when a Now Playing session first appears.
- Falls back to a default MediaWall screen, shuffled artwork, or an optional per-space Immich Kiosk display when nothing is playing.
- Provides a full-screen Wallpaper/Screensaver mode with library browsing, favorites, shuffle, logos, media info, transitions, and subtle backdrop motion.
- Uses Jellyfin backdrops/logos where available.
- Can use Navidrome playback for music-focused setups, and Navidrome can use Jellyfin's images when both services are configured.
- Can use local artist backdrop and logo files for Navidrome-only artwork.
- Caches grid/backdrop images with startup and cron-based library scans.
Jellyfin is recommended because MediaWall can use its rich backdrop, logo, user avatar, movie, series, and artist metadata. Navidrome isn't strictly required, and Jellyfin isn't strictly required for a music-only wall: Navidrome can drive Now Playing and local artist backdrop files can provide artwork. If both Jellyfin and Navidrome are configured, Navidrome playback can match against Jellyfin artist data so the Now Playing and Wallpaper/Screensaver views still benefit from Jellyfin's images.
Create a docker-compose.yml:
services:
mediawall:
image: ghcr.io/nothing2obvi/mediawall:latest
container_name: mediawall
restart: unless-stopped
ports:
- "1221:1221"
env_file:
- .env
volumes:
- ./config.yml:/app/config.yml:ro
- ./data:/app/data
- ./app/sounds:/app/sounds:ro
- ./app/custom_logo:/app/custom_logo:ro
- ./app/collections:/app/collections:ro
# Only needed when using Navidrome with local artist backdrop/logo files.
# - /path/to/your/navidrome/music:/navidrome_music:roThen:
- Copy
.env.exampleto.env. - Fill in Jellyfin and/or Navidrome connection values.
- Edit
config.ymlfor your spaces and libraries. - Start the app:
docker compose up -dOpen a space:
http://localhost:1221/livingroom
http://localhost:1221/office
http://localhost:1221/homelab
If a space has a password, pass it in the URL:
http://localhost:1221/livingroom?password=your-password
You can also open a mobile-friendly remote for any space by adding -remote to the space name:
http://localhost:1221/livingroom-remote
The remote follows the same password rule as the space, so a protected remote uses the same ?password= value.
Interactive state is stored in data/state.json. Image cache data is stored under the configured library scan directory, which defaults to /app/data/grid-cache inside the container.
These work in both Now Playing and Wallpaper/Screensaver mode unless noted.
| Key | Action |
|---|---|
f |
Enter or exit fullscreen. |
Escape |
Exit fullscreen. |
ArrowLeft |
Previous session or previous artwork. |
ArrowRight |
Next session or next artwork. |
Space |
Pause or resume the current mode. |
m |
Cycle through Now Playing, Wallpaper/Screensaver, and Immich Kiosk when that space has a Kiosk URL. |
t |
Cycle themes when the space uses theme: All or has no explicit theme. |
y |
Toggle local sound mute when sounds are enabled for the space. |
l |
Toggle logo display. |
i |
Toggle the media-info option for the current media type. |
a |
Toggle album art when available. |
s |
Toggle shuffle in Wallpaper/Screensaver mode. |
g |
Toggle the grid in Wallpaper/Screensaver mode. |
c |
Toggle the selection dialog in Wallpaper/Screensaver mode. |
ArrowUp / ArrowDown |
Switch libraries in Wallpaper/Screensaver mode when shuffle is off. |
These work in both Now Playing and Wallpaper/Screensaver mode.
| Gesture | Action |
|---|---|
| Tap or click left side | Previous session or previous artwork. |
| Tap or click right side | Next session or next artwork. |
| Tap or click center | Show the controls. |
| Triple-tap or triple-click center | Enter fullscreen, or exit fullscreen if already fullscreen. |
| Five quick center taps or clicks | Toggle local sound mute when sounds are enabled for the space. |
Open your MediaWall space in Safari, tap the Share button, then choose Add to Home Screen. Launching from the Home Screen runs it like a PWA. If sounds are enabled, tap the MediaWall screen once after opening so Safari allows audio playback.
On a regular computer, you can also open the space in a browser and make MediaWall fullscreen with the shortcuts listed below.
If animation changes seem to stick in the Home Screen app even though Safari shows the new behavior, delete the Home Screen app, go to Settings -> Safari -> Clear History and Website Data, open the MediaWall URL in Safari, refresh it, then add it to the Home Screen again.
Different devices handle motion differently. On older devices such as a 2017 iPad, start with simpler transitions like crossfade or fade, and gentler animations like breathe, pan, or focus. kenburns, drift, and the directional slide/push transitions can look great on faster displays, but they may feel heavier on older tablets.
Open your MediaWall space in Chrome, open the browser menu, then choose Add to Home screen or Install app if Chrome offers it. If sounds are enabled, tap the screen once after opening so the browser allows audio playback.
On devices with a keyboard or touch display, use the shortcuts in the Controls section above.
One common setup is to launch Chromium in kiosk mode after the desktop starts:
chromium-browser --kiosk --app=http://localhost:1221/livingroomUse the URL for the space you want to display. If the MediaWall container is running on another machine, replace localhost with that machine's IP address.
If you're not using kiosk mode, open the space in a regular browser and press f for fullscreen.
Once the container is running, you can test sounds, MediaWall fallback animations, and backdrop animations from inside the container:
docker exec mediawall npm run mediawall -- play sound noted.mp3 on livingroom
docker exec mediawall npm run mediawall -- play sounds All on livingroom
docker exec mediawall npm run mediawall -- play mediawall dvd on livingroom
docker exec mediawall npm run mediawall -- play screensaver All on livingroom
docker exec mediawall npm run mediawall -- play user transition on livingroom
docker exec mediawall npm run mediawall -- play user transition doug on livingroom
docker exec mediawall npm run mediawall -- animation pan on livingroom
docker exec mediawall npm run mediawall -- animation all --random on livingroomThe MediaWall fallback test displays the selected fallback for 30 seconds. All previews each configured MediaWall fallback animation for 30 seconds each and labels the current one in the bottom-right corner. Animation previews use the current backdrop by default; add --random to pick a random backdrop for the preview. play sounds All plays each available sound with two seconds between sounds and labels the current sound in the bottom-right corner. play user transition previews the Now Playing user-intro overlay. The command reads config.yml, so it can target password-protected spaces without putting the password in the command.
Every display space has a matching remote URL. For example, /livingroom has /livingroom-remote. The remote is meant for a phone or small tablet, works vertically or horizontally, and exposes the main controls: previous/next, pause/play in Wallpaper/Screensaver mode, mode, selection, grid, shuffle, favorites, media info, fullscreen, and sound mute when sounds are enabled.
The remote controls the same server-owned presentation state as the display. Browsers connected to the same space share the current mode, item/session, backdrop position, pause state, and transition deadline; another space keeps its own independent timeline. MediaWall uses a lightweight server-sent events connection to wake clients when that shared state changes, plus shared server timestamps so it doesn't need to send timer ticks every second.
Dialogs and the grid can be dismissed by tapping outside them or tapping the same remote button again. The grid uses the same libraries, favorite filtering, and password rules as the display. Remote button presses also show the same brief center-screen feedback icons on the display.
Display and remote routes install as distinct PWAs. A route ending in -remote uses the remote icon and its own manifest identity; normal space routes use the MediaWall logo. Query-string passwords are preserved in the launch URL but aren't used when deciding which icon/identity applies.
MediaWall v0.3 adds Immich Kiosk as an optional Now Playing fallback. It isn't the default and is configured independently for each space. When a configured space has no active sessions, MediaWall embeds the existing Immich Kiosk application across the full display and hides its own on-screen controls. Immich Kiosk keeps its own slideshow, menus, touch handling, and other interactions; MediaWall doesn't recreate them.
spaces:
homelab:
now_playing:
fallback: immich_kiosk
immich_kiosk:
url: "${HOMELAB_IMMICH_KIOSK_URL}"Put the actual URL in .env, especially when it contains an Immich Kiosk password:
HOMELAB_IMMICH_KIOSK_URL=https://immich-kiosk.example.com/?password=replace-meMediaWall keeps checking Jellyfin and Navidrome while Kiosk is visible. Playback automatically brings Now Playing back; when playback ends, Kiosk returns. A configured Kiosk also becomes a third display mode alongside Now Playing and Wallpaper/Screensaver.
If you choose a different album from Immich Kiosk's links, that choice survives temporary MediaWall handoffs but not a browser or PWA refresh. A container restart only loses it when the display page reloads. To make an album permanent, use that album as the main immich_kiosk.url.
The Kiosk URL must be reachable from both the MediaWall container and the display browser, and it must allow iframe embedding. If it can't be loaded, MediaWall uses its normal idle screen. Use the matching MediaWall remote when you need controls while Kiosk is on screen.
MediaWall v0.3 includes the existing default appearance plus 20 selectable themes: Dracula, Nord, Catppuccin Latte, Catppuccin Mocha, Gruvbox Dark, Gruvbox Light, Solarized Dark, Solarized Light, Tokyo Night, One Dark, Monokai, Rose Pine, Everforest, Kanagawa, Synthwave 84, Material Palenight, Night Owl, Ayu Mirage, GitHub Light, and Tomorrow Night.
Set theme: All on a space, or omit theme, to let viewers change it using the Themes button or the t key. The Themes button sits immediately before the version number and stays open while choices are applied, so colors can be compared quickly. The selected theme is stored in the synchronized space state: displays, remotes, reconnects, and refreshes all use the same choice. The remote includes the same selector and an explicit Cancel button. Selecting the current theme again does nothing and doesn't create another toast.
Set a specific theme name to lock that space to it. Fixed-theme spaces hide the Themes button, ignore t, and reject interactive theme changes from remotes or browsers. Themes change text, secondary text, accent colors, borders, controls, dialogs, toasts, and avatar outlines. They don't alter media artwork. Existing translucent surfaces, text shadows, and contrast treatments remain in place so both light and dark themes stay legible over changing backdrops.
Current compose examples mount ./app/sounds, ./app/custom_logo, and ./app/collections separately. Add custom sounds to app/sounds so MediaWall has one canonical sound directory at /app/sounds. Mounting the whole /app directory is not recommended because it can hide the application files inside the container.
Secrets should live in .env. Display behavior should live in config.yml.
For the complete configuration reference, including every section, option, default, required value, and notes, see CONFIGURATION.md. Optional settings can be omitted from config.yml; MediaWall uses the documented defaults.
JELLYFIN_URL=http://jellyfin.example.local:8096
JELLYFIN_API_KEY=replace-with-a-jellyfin-admin-api-key
JELLYFIN_USER=mediawall
NAVIDROME_URL=http://navidrome.example.local:4533
NAVIDROME_USER=mediawall
NAVIDROME_PASSWORD=replace-with-a-navidrome-password
LIVINGROOM_PASSWORD=
HOMELAB_PASSWORD=
LOG_LEVEL=infoThe Jellyfin API key should belong to a Jellyfin admin user. MediaWall uses it to read sessions, users, libraries, and artwork. Individual Jellyfin display users can still be selected per MediaWall user in config.yml, including All.
For Navidrome, configure each Navidrome account you want MediaWall to distinguish as its own MediaWall user. Navidrome users are what let MediaWall show unique sessions and user names when more than one person is listening. For example, you can add NAVIDROME_JON_USER, NAVIDROME_JON_PASSWORD, NAVIDROME_GUEST_USER, and NAVIDROME_GUEST_PASSWORD, then reference those from separate users entries in config.yml.
Password environment variables are per space by convention. In addition to LIVINGROOM_PASSWORD= and HOMELAB_PASSWORD=, you can create any others you need, such as OFFICE_PASSWORD= or KITCHEN_PASSWORD=, then reference them from the matching space.
Use LOG_LEVEL=debug when troubleshooting playback, session cycling, scans, or artwork behavior. Supported values are debug, info, warn, error, and silent; the default is info.
Jellyfin gives the best experience because MediaWall can access:
- Now Playing sessions
- user avatars
- movie, series, episode, and music metadata
- backdrops
- logos
- image tags for cache-busting changed artwork
Navidrome can be used for music playback. For artwork, MediaWall can use matching Jellyfin artist data when both services are configured, or local artist backdrop files when running Navidrome-only.
For Navidrome-only installations, enable:
navidrome:
artwork:
local_files: trueThen add at least one path_mappings mapping whose mediawall value points to the local root containing artist folders:
navidrome:
artwork:
path_mappings:
- navidrome: "/music"
mediawall: "/navidrome_music"Older configs using jellyfin in a path mapping are still accepted for compatibility, but new configs should use mediawall.
Node.js 22 or later is required. MediaWall's disk-backed Jellyfin collection index uses Node's built-in SQLite support.
Install dependencies:
npm installRun checks:
npm run typecheck
npm run buildRun locally:
npm run devStarting with v0.2, MediaWall is licensed under the GNU Affero General Public License v3.0. MediaWall remains open source and may still be used commercially, but modified versions used over a network must make the corresponding source code available under the terms of the AGPLv3. Previously released MIT versions remain under the license that accompanied those releases.
Contributions welcome.











