20 KiB
Executable File
ferret
A modern, accuracy-first video player for Linux, written in Rust.
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.
- Author: Jeremy Anderson
- Website: http://git.dcos.net/dcosnet/ferret
- License: GPL-2.0-or-later
Table of Contents
- Why ferret?
- Architecture
- Features
- Crates
- Build
- Run
- Keyboard Shortcuts
- Menu Reference
- Control Bar
- A/B Markers and Loop Export
- Subtitles
- Video Transforms
- Accuracy-First Error Policy
- Configuration
- Roadmap
- Contributing
- License
Why ferret?
ferret sidesteps three root causes of playback stutter on Linux:
- 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. - 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. - 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 (Bgra8Unorm → Rgba8Unorm → 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
#FF6464for A, blue#64B4FFfor 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
.txtor.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
.txtor.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
- Labels —
1: eng [default],2: Commentary, etc.
Video Transforms
- Rotation — 0°, 90°, 180°, 270° (mpv
video-rotateproperty) - Flip Horizontal — mirror left-right (mpv
vffilterhflip) - Flip Vertical — upside-down (mpv
vffiltervflip)
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 tracks —
1: eng [default],2: Commentary, etc.
Subtitles menu
- Load Subtitle File... — in-UI file browser (subtitle extensions)
- Show Subtitles
(V)— togglesub-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:SStime 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
- Play to the point where you want marker A.
- Press
[(or click File → Set A Marker). - Play to the point where you want marker B.
- 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
hflipfilter (mirror left-right) - Flip Vertical — adds/removes the
vflipfilter (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
- 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). - Config file (
~/.config/ferret/ferret.toml). - Playlist UI — drag-drop, reorder, repeat, shuffle.
- Media keys (MPRIS on Linux).
- 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.
/ferret/ferret-ss.png)