ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts.
Go to file
Jeremy Anderson 21979db4ea updated screenshot 2026-07-29 23:52:46 -04:00
crates ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
scripts ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
.gitignore ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
BLOG.md ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
CONTRIBUTING.md ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
Cargo.lock ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
Cargo.toml ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
LICENSE ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
QUICKSTART.md ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00
README.md updated screenshot 2026-07-29 23:52:46 -04:00
clippy.toml ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts. 2026-07-29 23:46:01 -04:00

README.md

ferret

A modern, accuracy-first video player for Linux, written in Rust.

License: GPL-2.0 Version Rust

ferret

ferret is a desktop video player built on libmpv, with a custom egui overlay UI rendered in a transparent always-on-top window. The engine runs on its own thread; the UI never blocks playback. File dialogs are drawn inside the overlay — no external dialog processes, no z-order conflicts.


Table of Contents


Why ferret?

ferret sidesteps three root causes of playback stutter on Linux:

  1. Copy-back hardware decoding shuttling frames over the PCIe bus. ferret sets hwdec=auto-safe — only zero-copy hardware decode paths (VA-API/NVDEC/Vulkan video decode), never copy-back modes.
  2. Software audio amplification conflicting with PipeWire's logarithmic volume curves. ferret hard-caps volume at 100% (volume_max=1.0); the volume slider maps 1:1 with the system sink.
  3. Push-through error handling causing visual smearing on corrupt frames. ferret sets framedrop=vo — drop frames only at display, never on decode. Corrupt frames are dropped, never smeared through.

The engine thread owns libmpv exclusively; UI thread hangs never cause AV stutter because the engine has no knowledge of the UI.


Architecture

┌─────────────────────────────────────────────────────────────┐
│ ferret binary (player-app)                                  │
│                                                             │
│  winit event loop (main thread)                             │
│  ├─ Video window ──wid──▶ libmpv ──▶ FFmpeg/Vulkan          │
│  │                          │                                │
│  │                          ├─ decode                       │
│  │                          ├─ AO (PipeWire/PulseAudio)     │
│  │                          └─ VO (Vulkan/OpenGL)           │
│  │                                                          │
│  └─ Overlay window (egui + wgpu, transparent, AlwaysOnTop)  │
│     ├─ Control bar (play/pause, seek, volume, markers)       │
│     ├─ Menu bar (File / Playback / Audio / Subtitles / etc)  │
│     └─ In-UI file browser (replaces external file dialogs)   │
│                                                             │
│  Engine thread (named "ferret-engine")                      │
│  └─ owns MpvHandle, drains libmpv event queue,              │
│     publishes EngineEvents on a bounded channel             │
│                                                             │
│  ffmpeg worker thread (named "ferret-ffmpeg")               │
│  └─ spawned for A-B loop video export, sends result back    │
└─────────────────────────────────────────────────────────────┘

Threading model: thread-per-subsystem with crossbeam channels. No async.

  • Cmd channel (UI → engine): bounded::<Cmd>(64) — bounded for natural backpressure.
  • Event channel (engine → UI): bounded::<EngineEvent>(256) — single consumer.
  • State snapshot: Arc<Mutex<PlaybackState>> (parking_lot) shared between threads.

Two-window model: libmpv renders directly into the video window via X11 wid embedding. The overlay window is transparent, borderless, and AlwaysOnTop — it covers the video window and renders the UI with egui + wgpu. Mouse clicks outside the control bar don't pass through (known limitation; fix requires the libmpv render-context API — on the roadmap).

X11 background pixel: the video window's X11 background pixel is set to #141416 via XSetWindowBackground before libmpv attaches. This prevents the X server from showing framebuffer garbage during window resize/move (the "mirrored desktop" artifact).

wgpu presentation: forced to PresentMode::Fifo (vsync) for compatibility. Surface format prefers non-sRGB (Bgra8UnormRgba8Unorm → sRGB variants) to avoid egui color-management warnings. Alpha mode is Auto.


Features

Core Playback

  • Play / Pause / Stop — buttons + keyboard + menu
  • Seek bar — drag-to-scrub, click-to-jump, hover preview line, A/B marker pins
  • Frame stepping — step forward/backward one frame at a time
  • Volume — slider + mute toggle, hard-capped at 100%
  • Fullscreen — borderless fullscreen toggle

Loop Modes

  • Off — play once, stop at EOF
  • Loop File — repeat the current file indefinitely (loop-file=inf)
  • Loop Playlist — repeat the entire playlist indefinitely (loop-playlist=inf)

Speed Control

  • Presets — 0.25×, 0.50×, 0.75×, 1.00×, 1.25×, 1.50×, 2.00×, 3.00×, 4.00× (menu)
  • Quick presets — 0.50×, 1.00×, 1.50×, 2.00× (control bar buttons)
  • Fine slider — 0.25× to 4.0× in 0.01 increments (control bar)
  • Keyboard- / = nudge by 0.25×

A/B Markers

  • Set A / Set B — drop markers at current playback position
  • Marker pins — drawn on the seek bar (red #FF6464 for A, blue #64B4FF for B)
  • Toggle A-B Loop — loop between markers (mpv's ab-loop-a / ab-loop-b)
  • Export A-B Loop Video — renders the segment to a new file via ffmpeg
  • Import/Export Markers — save/load marker positions as .txt or .json

File Menu

  • Load File — in-UI file browser (media extensions filtered)
  • Load Folder — enumerate video files in a folder as a playlist
  • Load Playlist — multi-select files (media + .m3u/.m3u8/.pls)
  • Export A-B Loop Video — ffmpeg-rendered clip (MP4/MKV/WebM)
  • Import Markers — load saved A/B positions from .txt or .json
  • Toggle Fullscreen
  • Quit

Subtitles

  • Embedded track selection — auto-detects from mpv's track-list
  • External subtitle loading.srt, .ass, .ssa, .sub, .idx, .sup, .vtt, .smi, .lrc
  • Visibility toggle — hide subs without losing the selected track
  • Forced track support — displays [forced] badge for forced tracks

Audio Tracks

  • Track selection — dropdown with all embedded audio tracks
  • Auto — let mpv pick the default track
  • Labels1: eng [default], 2: Commentary, etc.

Video Transforms

  • Rotation — 0°, 90°, 180°, 270° (mpv video-rotate property)
  • Flip Horizontal — mirror left-right (mpv vf filter hflip)
  • Flip Vertical — upside-down (mpv vf filter vflip)

In-UI File Browser

All file selection happens inside the egui overlay — no external dialog processes (zenity, kdialog, rfd). This eliminates the z-order conflict where external dialogs would pop under the overlay's AlwaysOnTop window. The browser supports:

  • Directory listing with sorted entries (directories first, then files)
  • Extension filtering per dialog kind
  • Single-select, multi-select (Load Playlist), and save modes (filename input)
  • Path bar with Up / Home navigation
  • Double-click to navigate into directories

Crates

Crate Role
mpv-bindings bindgen FFI to libmpv + safe wrapper (MpvHandle, Event, Property, Command)
player-core Headless engine: PlayerEngine, Cmd/EngineEvent channels, accuracy-first options, PlaybackState
player-ui egui + wgpu overlay renderer: control bar, menus, in-UI file browser, icons, theme
player-app Binary: multi-window winit event loop, X11 wid embedding, keyboard input, ffmpeg export

Build

Prerequisites

  • Rust 1.75+ (install via rustup)
  • libmpv 2.x (provided by setup-libmpv.sh)
  • libclang (for bindgen; provided by setup-libmpv.sh)
  • ffmpeg (for A-B loop video export; optional)
  • libX11 (linked directly for XSetWindowBackground; usually pre-installed)

One-time setup (libmpv without root)

cd /path/to/ferret
./scripts/setup-libmpv.sh
source ./mpv-prefix/env.sh

This downloads libmpv2, libmpv-dev, and all runtime dependencies, extracts them into ./mpv-prefix/, and writes ./mpv-prefix/env.sh. The script also pulls in winit's xkbcommon runtime deps and mesa software rasterizers.

Build the player

cargo build --release

The release binary is at target/release/ferret. rpath is baked in, so no LD_LIBRARY_PATH is needed at runtime.

Build flags

./scripts/build.sh           # build release (default)
./scripts/build.sh --debug   # build debug
./scripts/build.sh --clean   # clean + rebuild
./scripts/build.sh --test    # run cargo test --release
./scripts/build.sh --lint    # cargo clippy + bracket audit + deref audit
./scripts/build.sh --ci      # lint + test + build (full CI pass)

Build profiles

[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
strip = "symbols"
panic = "abort"

[profile.dev]
opt-level = 1    # wgpu/shaders benefit from at least -O1
debug = 1

Run

ferret /path/to/video.mp4

Or launch with no arguments to get an empty dark grey window with the menu bar — use File → Load File... to open the in-UI file browser.


Keyboard Shortcuts

Every hotkey has a menu entry and/or a control-bar button. Menu items show the shortcut in parentheses (e.g. Play / Pause (Space)).

Key Action Menu / Control
Space Play / pause Playback menu, ▶/⏸ button
Q Quit File menu
F Toggle fullscreen File menu, ⛶ button
M Toggle mute Playback → Volume, 🔊 button
← / → Seek -5s / +5s (keyframes) Playback → Seek
↑ / ↓ Volume +5% / -5% Playback → Volume
, / . Frame-step back / forward Playback → Seek, ◀
[ / ] Set A / B marker File menu, A/B buttons
\ Clear markers File menu
L Cycle loop mode (off/file/list) Playback → Loop, ⟳ button
- / = Speed -0.25× / +0.25× Playback → Speed, slider
N / P Playlist next / previous Playback → Playlist, ⏭/⏮ buttons
V Toggle subtitle visibility Subtitles menu

Menu Reference

The menu bar is always visible at the top-left, even when the bottom control bar has auto-hidden.

File menu

Item Shortcut Section
Load File... Open
Load Folder... Open
Load Playlist... Open
Set A Marker [ Markers
Set B Marker ] Markers
Clear Markers \\ Markers
Toggle A-B Loop Markers
Export A-B Loop Video... Markers
Import Markers... Markers
Toggle Fullscreen F Window
Quit Q

Playback menu

Item Shortcut Section
Play / Pause Space
Stop
Forward 5s Seek
Backward 5s Seek
Frame Step Forward . Seek
Frame Step Backward , Seek
Next N Playlist
Previous P Playlist
Volume Up 5% Volume
Volume Down 5% Volume
Toggle Mute M Volume
Off / Loop File / Loop Playlist Loop
Cycle Loop Mode L Loop
Speed Up +0.25× = Speed
Speed Down -0.25× - Speed
0.25× through 4.0× presets Speed

Audio menu

Appears only when the loaded file has audio tracks.

  • Auto — let mpv pick the default track
  • List of embedded audio tracks1: eng [default], 2: Commentary, etc.

Subtitles menu

  • Load Subtitle File... — in-UI file browser (subtitle extensions)
  • Show Subtitles (V) — toggle sub-visibility
  • None — disable subtitles
  • List of embedded subtitle tracks — with [forced] / [default] badges

Video menu

  • Rotate: 0° (normal), 90°, 180°, 270°
  • Flip: Flip Horizontal (mirror), Flip Vertical (upside-down)

Help menu

  • About ferret — toggles the About panel (version, author, license)
  • Version: ferret 1.0.0
  • License: GPL-2.0-or-later

Control Bar

The bottom control bar (118px tall) has three rows:

Row 1 — Seek bar

  • Current time label (monospace)
  • Seek bar with A/B marker pins (red for A, blue for B)
  • Duration label (monospace)

Row 2 — Transport | time | volume + fullscreen

  • Play/Pause, Stop, Previous, Next, Frame Back, Frame Forward
  • Center: MM:SS / MM:SS time display
  • Right: Fullscreen button, Volume slider, Volume/mute icon

Row 3 — Markers | loop | speed | audio

  • A marker button (shows time or "A"), B marker button (shows time or "B")
  • AB-loop toggle, Loop-mode toggle (off/file/playlist)
  • Speed presets: 0.50×, 1.00×, 1.50×, 2.00×
  • Fine speed slider (0.25×4.0×), current speed label
  • Audio track dropdown

A/B Markers and Loop Export

Setting Markers

  1. Play to the point where you want marker A.
  2. Press [ (or click File → Set A Marker).
  3. Play to the point where you want marker B.
  4. Press ] (or click File → Set B Marker).

The markers appear as colored pins on the seek bar — red for A, blue for B.

Looping A→B

When both markers are set, mpv automatically loops between them. The Toggle A-B Loop menu item (or the AB-loop button in the control bar) turns this on/off. When toggled off, the markers are cleared.

Exporting the A-B Loop

File → Export A-B Loop Video... opens the in-UI save dialog, then runs ffmpeg to extract the segment:

ffmpeg -y \
  -ss <A> \
  -i <input> \
  -t <duration> \
  -c:v libx264 -preset fast -crf 18 \
  -c:a aac -b:a 192k \
  <output>

-ss is placed before -i for fast seeking. Video is re-encoded (libx264, CRF 18) for frame accuracy and maximum compatibility. Audio is re-encoded to AAC at 192 kbps. ffmpeg must be installed and in your PATH. The export runs on a background thread (ferret-ffmpeg) so the UI stays responsive.

Importing/Exporting Markers

Markers can be saved to disk and reloaded later:

Text format (.txt):

# file: /path/to/video.mp4
# duration: 180.000s
# speed: 1.00x

A 00:01:23.456
B 00:02:45.000
LOOP on

JSON format (.json):

{"file":"/path/to/video.mp4","duration":180.000,"speed":1.000,"a":83.456,"b":165.000,"loop":true}

Subtitles

ferret supports both embedded subtitle tracks (from the container) and external subtitle files.

Embedded Tracks

When a file with subtitle tracks is loaded, the Subtitles menu populates with all available tracks. Each track shows its id, language, and any badges ([forced], [default]).

External Subtitles

Subtitles → Load Subtitle File... opens the in-UI file browser filtered for: .srt, .ass, .ssa, .sub, .idx, .sup, .vtt, .smi, .lrc. The loaded subtitle becomes active immediately.

Visibility

Subtitles → Show Subtitles (V) toggles sub-visibility — hides subtitles without losing the selected track, useful for quickly checking the raw video.


Video Transforms

Rotation

Video → Rotate sets the video-rotate mpv property. Values: 0°, 90°, 180°, 270°. The rotation is applied immediately and reflected in the state.

Flip

Video → Flip toggles mpv video filters:

  • Flip Horizontal — adds/removes the hflip filter (mirror left-right)
  • Flip Vertical — adds/removes the vflip filter (upside-down)

The filters are managed by reading the current vf property, adding or removing the filter name, and writing it back.


Accuracy-First Error Policy

ferret configures libmpv for visual accuracy over continuity:

  • hr-seek=yes — exact seeks, not keyframe-snapped. Seeks take slightly longer but land on the requested frame.
  • framedrop=vo — only drop frames at the video output if behind. Never drop on decode, so corrupt frames don't get smeared through.
  • video-sync=display-resample — resample audio to match the display refresh rate. Best A/V sync, eliminates audio drift.
  • hwdec=auto-safe — use hardware decoding only when known-safe (no copy-back modes that shuttle frames over the PCIe bus).
  • volume-max=100 — hard-coded. No software amplification past 100%, killing the PipeWire volume clash.

These are baked into EngineOptions::default() and set as mpv options at engine init time. User config (~/.config/mpv/mpv.conf) is explicitly disabled (config=no) to prevent sneaking in different behavior.

Additional fixed mpv options: terminal=no, msg-level=all=<level>, input-default-bindings=no, input-builtin-bindings=no, osc=no, cursor-autohide=no, input-vo-keyboard=no, vo=gpu.


Configuration

All options are code-defined in EngineOptions::default(): (crates/player-core/src/options.rs)

Field Default Purpose
hr_seek true Exact seeks
framedrop "vo" Drop at display only
video_sync "display-resample" Resample audio to display
hwdec "auto-safe" Safe hardware decode only
initial_volume 1.0 (100%) Startup volume
volume_max 1.0 (100%) Hard cap — never higher
log_level "warn" libmpv log threshold
wid None Set at runtime from video window XID
vo "gpu" Video output driver
loop_mode Off Initial loop mode

There is no config file yet (planned for a future release).


Roadmap

Done (v1.0)

  • Multi-window winit + egui overlay (X11)
  • libmpv 2.x FFI via bindgen
  • Engine thread with full property observation
  • Play/pause/stop/seek/frame-step
  • Volume + mute (hard-capped at 100%)
  • Loop modes (off/file/playlist)
  • Speed control (presets + slider, 0.25×4×)
  • A/B markers + loop + video export via ffmpeg
  • Marker import/export (txt + json)
  • Audio track selection
  • Subtitle track selection + external loading
  • Video rotation (0/90/180/270) + flip (H/V)
  • In-UI file browser (no external dialog dependency)
  • Complete hotkey coverage — every shortcut has a menu entry and/or button
  • Accuracy-first error policy
  • X11 background pixel fix (eliminates resize/move "mirrored desktop" artifact)
  • wgpu surface error recovery + forced PresentMode::Fifo
  • CI tooling: bracket audit, deref audit, clippy config

Planned

  1. Wayland support via mpv_render_context + MPV_RENDER_API_TYPE_OPENGL. Eliminates the wid/X11 dependency, unblocks macOS (Metal via wgpu) and Windows (DX12 via wgpu).
  2. Config file (~/.config/ferret/ferret.toml).
  3. Playlist UI — drag-drop, reorder, repeat, shuffle.
  4. Media keys (MPRIS on Linux).
  5. Single-window compositing — render video as a wgpu texture inside egui, eliminating the second window and the click-through limitation.

Contributing

See CONTRIBUTING.md for development setup, code style, coding standards (PEP 868 / POSIX / SEI CERT / MISRA), and pre-commit checks.

Patches are welcome at http://git.dcos.net/dcosnet/ferret.


License

ferret is licensed under the GNU General Public License v2.0 or (at your option) any later version.

ferret - a modern, accuracy-first video player for Linux
Copyright (C) 2026 Jeremy Anderson

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

libmpv (linked dynamically) is licensed under LGPL-2.1+ or GPL-2+ — the dynamic linking keeps ferret's GPL-2.0 compatible.