medias 0.5.1

TV media player on bare DRM/KMS: Vulkan UI plane over libmpv video, for local files, UPnP/DLNA servers and YouTube, controlled by HDMI-CEC remote, evdev or terminal
medias-0.5.1 is not a library.

medias

A TV media player that runs directly on DRM/KMS, without X11 or Wayland. libmpv decodes video into one KMS plane. A Vulkan-rendered UI sits on a second plane, and both are composited in one atomic page flip. On the Raspberry Pi, hardware-decoded frames are scanned out directly, with HDR10 output. Input comes from an HDMI-CEC remote, evdev keyboards and remotes, or the terminal. Targets are the Raspberry Pi and desktop GPUs (e.g. AMD RX 6800); see Hardware.

Features

  • Local library: video and music folders with thumbnails (a frame at 15%, embedded or folder cover art). Per-file resume; search-as-you-type filter with an on-screen keyboard.
  • UPnP/DLNA: discovers media servers via SSDP (e.g. Fritz!Box, minidlna) and browses their ContentDirectory. Album art is shown; items play from their HTTP resources.
  • Folder playback: at the end of a file the next one in its folder plays (on by default, togglable). Next/Previous-track keys skip.
  • YouTube: search, trending, subscriptions, playlists, liked videos (via YouTube Data API v3 sign-in), and "Like your watched" (videos related to watched ones). Streams play directly from InnerTube, with yt-dlp as fallback. The original audio track plays first; dubs in your languages are offered as named tracks.
  • Refresh-rate matching: during playback the display switches to a same-size mode at a multiple of the video frame rate (23.976 fps → 23.976 Hz, 25 → 50), and back in the library.
  • Direct scanout and HDR (Raspberry Pi 4/5 planes): hardware-decoded frames go to their own KMS plane without a GL pass; 4K HEVC Main 10 plays at 24 fps on the Pi 5. PQ and HLG video switch an HDR TV to BT.2020 HDR with the file's static metadata; the UI is drawn at SDR brightness on top.
  • Views: text list, thumbnail list, or a wide thumbnail grid (Yellow/Info toggles).
  • Black screen: Yellow/Info during playback cycles info shown → hidden → black screen. Audio keeps playing, which suits music on OLED TVs.

Requirements

  • Linux with a KMS driver and an EGL/GLES driver (video).
  • Optional: a Vulkan driver with dmabuf import (e.g. Mesa RADV or V3DV, or NVIDIA) for the UI. Without one (e.g. Mali Midgard on RK3399) a CPU rasterizer draws the UI; MEDIAS_UI=cpu forces it.
  • libmpv (package mpv or libmpv-dev). libgbm, libEGL and the Vulkan loader are loaded or linked from the system.
  • Access to /dev/dri/card*, /dev/dri/renderD*, /dev/input/event*, and to /dev/cec* for HDMI-CEC (groups video, render, input).
  • No desktop or display manager holding the display (DRM master); otherwise startup fails with acquire DRM master ... Permission denied. sudo fuser -v /dev/dri/card* shows the holder. Stop it with sudo systemctl stop display-manager, or boot to a console with sudo systemctl set-default multi-user.target.
  • Optional: yt-dlp for the playback fallback and anonymous YouTube search.

Hardware

Software-decoded video renders through mpv's OpenGL path at up to 1080p; the display scales it. GPU fill rate limits that path more than decoding does. Hardware-decoded video on the Pi bypasses GL (direct scanout). Measurements: PERFORMANCE.md.

Status Hardware Notes
Tested Raspberry Pi 5 Vulkan UI (V3DV). HEVC hwdec with direct scanout: 4K Main 10 at 24 fps, HDR10 (LG TV). H.264 1080p25 software.
Tested Desktop: AMD (RADV), NVIDIA Vulkan UI.
Expected Raspberry Pi 4 Vulkan UI (V3DV); H.264 and HEVC hwdec, direct scanout. Untested.
Expected RK3588/RK3588S (e.g. Radxa Rock 5C) Mali-G610: PanVK Vulkan, Panthor kernel driver (recent mainline). 4x A76 decode 1080p in software. Untested. Not RK3582 (Rock 5C Lite): its GPU is disabled.
Best effort Allwinner H618 (Orange Pi Zero3) Mali-G31, no Vulkan: CPU UI. 1080p24 H.264 needs profile=fast (MEDIAS_MPV_CONF); 30 ms of a 41.7 ms frame. Set MEDIAS_AUDIO_DEVICE (e.g. alsa/plughw:CARD=HDMI,DEV=0).
Best effort RK3399 (e.g. Asus Tinker Edge R) Mali-T860, no Vulkan: CPU UI. Too slow on vendor kernels; mainline untested.

For a new 1080p box, buy a Raspberry Pi 4 or 5, or an RK3588 board.

Install

cargo install medias
# with the "For you" feed (YouTube's TV OAuth client, see below):
cargo install medias --features youtube-tv

Release builds for Linux x86_64 and aarch64 (tar.gz and Debian 13 trixie .deb, in dist/) need cargo-make, cargo-zigbuild, zig, binutils, and the host's libmpv and libgbm (their symbols form the link stubs):

cargo make dist                          # all targets; cargo make dist --help
sudo apt install ./dist/medias_*_arm64.deb

Apt repository: cargo make repo-key creates the signing key once (ed25519, passphrase). cargo make repo adds dist/*.deb to dist/debian/pool and writes the signed indexes, medias.asc and medias.sources (key embedded). The pool keeps every version; upload without --delete:

cargo make dist && cargo make repo

On a Debian 13 trixie as root:

curl -fsSL https://www.creative-memory.ai/andy/medias/dist/debian/medias.sources -o /etc/apt/sources.list.d/medias.sources
apt update && apt install medias

Service

medias@.service runs medias on tty1 as the instance's user, with a logind session (PipeWire, device access) and groups video render input audio. Options go in /etc/default/medias (MEDIAS_ARGS, MEDIAS_* variables).

# Edit /etc/default/medias to configure.
# Replace USER with the user you want medias (and e.g. pipewire) to run as. 
sudo systemctl enable --now medias@$USER.service

The .deb installs the unit to /usr/lib/systemd/system. From the tar.gz, copy medias to /usr/bin, medias@.service to /etc/systemd/system and medias.default to /etc/default/medias.

Run

medias                                   # ~/medias/Videos and ~/medias/Music
medias --videos=Movies=/mnt/movies --videos=Series=/mnt/series --music=/mnt/music
medias metadata movie.mkv                # codecs, resolution, fps, bit depth, channels, bitrates
medias planes                            # TV HDR capabilities, KMS planes and their formats
medias --help                            # all options, keys and environment variables

The same command works from a Linux console, SSH or a terminal multiplexer. scripts/medias-vt optionally starts it on a separate VT (see scripts/README.md).

Environment variables

Set them in the shell, or in /etc/default/medias for the service.

Display and video:

Variable Effect
MEDIAS_DIRECT=0 Render hardware-decoded frames through GL. Default: direct scanout where the planes take the Pi decoder's 10-bit format (P030); 1 tries it on other hardware.
MEDIAS_HDR=0 No HDR signalling. Default with direct scanout: PQ/HLG video switches the display to BT.2020 HDR when the TV's EDID lists the transfer function.
MEDIAS_HDR_WHITE=<cd/m2> Brightness of the UI's white in HDR mode (default 100). Lower it if the UI looks brighter than in SDR; 25 matched SDR on an LG TV.
MEDIAS_MATCH_RATE=0 Keep the display refresh rate during playback.
MEDIAS_MATCH_RATE_DELAY=<s> Pause after a refresh or HDR switch while the TV resyncs (default 3; 0: none).
MEDIAS_HWDEC=auto|no|... mpv hardware decoding (default auto; no: software).
MEDIAS_UI=cpu Draw the UI with the CPU rasterizer instead of Vulkan (automatic without a Vulkan device that imports dmabufs).
MEDIAS_MPV_CONF=<path> mpv options, one key=value per line, applied last (e.g. profile=fast for slow GPUs).

Audio:

Variable Effect
MEDIAS_AO=<list> mpv audio output list (default pipewire,alsa,null).
MEDIAS_AUDIO_DEVICE=<dev> mpv audio device (aplay -L lists names, e.g. alsa/sysdefault:CARD=vc4hdmi0).

YouTube:

Variable Effect
MEDIAS_YT_MAX_HEIGHT=<px> Stream height cap (default 1080).
MEDIAS_YT_CODECS=<list> Allowed video codecs, preferred first (default av01,vp9,avc1; Pi: avc1).
MEDIAS_YT_AUDIO_LANGS=<list> Languages whose dubs are offered beside the original audio, e.g. de,en (default: from the locale).
MEDIAS_YT_SUB_LANGS=<list> Caption languages offered (default: MEDIAS_YT_AUDIO_LANGS).
MEDIAS_YT_SYNC=1 Report playback to the YouTube TV account's watch history (youtube-tv builds).
MEDIAS_YT_CLIENT_ID, MEDIAS_YT_CLIENT_SECRET, MEDIAS_YT_API_KEY Data API credentials, instead of youtube_oauth.json.
MEDIAS_YTDL_PATH=<path> yt-dlp binary (default: found in /usr/bin etc.).
MEDIAS_YTDL_FORMAT=<format> yt-dlp format for the playback fallback (default bestvideo[height<=?1080]+bestaudio/best).

Diagnosis:

Variable Effect
MEDIAS_STATS=1 Print per-second playback stats: frames presented, render times, drops, decoder.
MEDIAS_MPV_LOG=<path> Write mpv's verbose log.
MEDIAS_GL_SYNC=flush glFlush instead of glFinish after each GL video frame.
MEDIAS_YT_DEBUG=1 Dump InnerTube browse responses to ~/medias/innertube-*.json.

MEDIAS_ARGS in /etc/default/medias holds the command-line options of the service (split at whitespace).

Files under ~/medias

Path Purpose
Videos/, Music/ default library roots
upnp.txt UPnP server description URLs outside multicast reach, one per line
youtube.txt YouTube sources, Label | query per line (yt-dlp query or URL)
youtube_oauth.json Data API sign-in: {"client_id", "client_secret", "api_key"?}
.history resume positions and last-played dates
.youtube_auth, .youtube_tv_auth stored sign-in sessions (mode 0600)
.view, .autoplay list/grid view (default grid); play-next-in-folder on/off
.cache/thumbs/ thumbnail cache

YouTube sign-in

  • Data API (subscriptions, playlists, liked videos): create a Google Cloud OAuth client of type "TVs and Limited Input devices", enable YouTube Data API v3, and put its ID and secret in ~/medias/youtube_oauth.json. "Sign in" then shows a code and a QR code. In the consent screen's "Testing" state, Google expires the refresh token after 7 days.
  • For you (build feature youtube-tv): a second sign-in with YouTube's own TV client (as the Kodi YouTube plugin does). No configuration is needed. The feed loads up to 36 videos. The same sign-in adds "Watched" and "Playlists" (own and saved, up to 200 entries each). MEDIAS_YT_SYNC=1 adds videos played in medias to that account's YouTube watch history (off by default).
  • Without either, search uses yt-dlp anonymously.

Remote keys (summary)

Key Browse Playback
Up/Down move volume
Left/Right page (grid: move) seek ±10 s
OK open / play playback menu
Back up a level back to the library (paused)
Rewind/Fast-forward, Channel ± page seek ±60 s
Forward/Backward (Next/Previous track) page next/previous file in folder
Red / Green — subtitles / audio track
Blue on-screen keyboard playback menu
Yellow list/grid view info → hidden → black screen

License

GPL-2.0-or-later (LICENSE). The embedded Liberation Sans font is under the SIL Open Font License 1.1 (assets/LiberationSans-OFL.txt).