ferret/README.md

594 lines
20 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ferret
**A modern, accuracy-first video player for Linux, written in Rust.**
[![License: GPL-2.0](https://img.shields.io/badge/license-GPL--2.0-blue.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.0.0-orange.svg)](#)
[![Rust](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org)
![ferret](/home/dcosnet/Desktop/MASTERSONGIT/ferret-audited(16)/ferret/ferret-ss.png)
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?](#why-ferret)
- [Architecture](#architecture)
- [Features](#features)
- [Crates](#crates)
- [Build](#build)
- [Run](#run)
- [Keyboard Shortcuts](#keyboard-shortcuts)
- [Menu Reference](#menu-reference)
- [Control Bar](#control-bar)
- [A/B Markers and Loop Export](#ab-markers-and-loop-export)
- [Subtitles](#subtitles)
- [Video Transforms](#video-transforms)
- [Accuracy-First Error Policy](#accuracy-first-error-policy)
- [Configuration](#configuration)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
---
## 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 (`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 `#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
- **Labels** — `1: 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](https://rustup.rs))
- **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)
```bash
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
```bash
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
```bash
./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
```toml
[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
```bash
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, ◀| / |▶ buttons |
| [ / ] | 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)` — 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:
```bash
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):**
```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](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](LICENSE)
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.