Terminal version
+----+ > VHS-GUI +------------------------------------------------------------+
| |
| ##. ##. ##. ##. #######. ######. ##. ##. ##. |
| ##: ##: ##: ##: ##...... ##...... ##: ##: ##: |
| ##: ##: #######: #######. #####. ##: ###. ##: ##: ##: |
| .##. ##.. ##...##: .....##: ...... ##: ##: ##: ##: ##: |
| .####.. ##: ##: #######: .######.. .######.. ##: |
| ..... ... ... ........ ....... ....... ... |
| |
| .:######################################################:. |
| > PLAY - LP 0:47:12 TRK ===.. * REC |
+-----------------------------------------------------------------------------+
cat banner.ans in a 256-color terminal for the vibrant ANSI-colored version.
A native Rust desktop application that wraps VHS digitization — capture, processing, playback, and AI upscaling — in a single window. It's the primary day-to-day interface for tape digitization.
This is a self-contained Rust application. It does not call or depend on any
external pipeline scripts. Operations it performs (deinterlace, upscale, denoise,
etc.) are implemented natively in Rust, spawning only the underlying media tools
directly (ffmpeg, VapourSynth's vspipe, Real-ESRGAN) — never a wrapper script.
This is a deliberate, ongoing migration (see "Native-Rust port status" below); some
operations still shell out to scripts from the sibling
vhs-cli repo as a transitional measure while
that migration is in progress, tracked explicitly in src/config.rs.
There's also a bash/CLI version of this same pipeline, vhs-cli — useful for terminal/scripted workflows. The two are independent sibling projects; this repo has no dependency on that one beyond the transitional script paths noted above.
Expected local layout: this repo lives at ~/Videos/vhs-gui/, a sibling of the
data directories (~/Videos/captures/, ~/Videos/logs/, etc.) which are not part of
this repo — they're local working directories referenced by absolute path from
src/config.rs.
Built and run on a single Linux workstation:
- CPU: AMD Ryzen 9 5900X (12-core).
- GPU: AMD Radeon RX 7800 XT (RDNA 3, gfx1101) — drives Real-ESRGAN upscale jobs via either backend (ROCm/PyTorch or Vulkan/ncnn; see the Upscale Settings panel). ROCm is required for the community VHS-specific models — the Vulkan binary segfaults on any model name outside its hardcoded family list.
- VHS capture device (the flakiest link in the pipeline): MacroSilicon
MS210x USB video grabber — a low-cost USB2.0 analog capture dongle
("EasierCAP"-type). Opened via V4L2 at a stable
/dev/v4l/by-id/...path (src/config.rs'sv4l2_devicedefault) so it survives USB port changes; override it for different capture hardware. A fair amount of this app's capture-side code exists specifically to work around this device's quirks:- Exclusive access. Only one process can hold its V4L2 fd at a time, so
handing off from the live Monitor preview (mpv) to a capture (ffmpeg)
can't just start the second process — mpv's hold has to actually clear
first. That's the
Releasingcapture state (1s timeout,src/panels/monitor.rs) between Monitoring and Capturing. - Spurious preview drops. The live-preview stream sometimes goes idle
on its own mid-capture even though ffmpeg is still recording correctly
in the background. Rather than surface that as an error, an "insurance
reopen" (throttled to once per 2s,
src/panels/monitor.rs) automatically reconnects mpv to the preview stream. - Slow USB control transfers. Setting brightness/contrast/hue/etc. via
VIDIOC_S_CTRLcan block for multiple seconds on this device.src/ v4l2.rsissues every control write from a dedicated background thread over a bounded channel specifically so a slow ioctl never freezes the UI thread — see the comment above the thread spawn there. - Video and audio are two independent USB interfaces on the same dongle, not a hardware-synced A/V pair — they drift over a long capture. Native "Fix A/V Sync" corrects this after the fact.
- Exclusive access. Only one process can hold its V4L2 fd at a time, so
handing off from the live Monitor preview (mpv) to a capture (ffmpeg)
can't just start the second process — mpv's hold has to actually clear
first. That's the
- Not used by this app, but worth noting: the same machine also has a
Blackmagic Design Intensity Pro PCIe capture card, used only by vhs-cli's
separate OBS
gameenv slot for console capture (unrelated to VHS digitization — this app never touches it). The cheap USB dongle above is the one that actually works; the "proper" dedicated capture card currently doesn't. Failure details and log evidence are in the vhs-cli README, since that's the repo that owns the OBS config and logs it's diagnosed from. - Upscale scratch storage: a secondary drive mounted at
/media/<user>/<external-drive>/Videos/vhs_upscale_work/— segment checkpoints for chunked/resumable upscale jobs live there by default.
cd vhs-gui
cargo build # debug
cargo build --release # optimized, for system install
DISPLAY=:0 ./target/debug/vhs-gui # run debug build
DISPLAY=:0 ./target/release/vhs-gui # run release buildcargo build --release
sudo install -m 0755 target/release/vhs-gui /usr/local/bin/vhs-gui
./install-desktop.sh # installs the .desktop entry + icon, points Exec at /usr/local/bin/vhs-guieframe/egui0.34 (glow/OpenGL backend, Wayland via winit)libmpv26.0.0 — render API, off-screen FBO, blitted via an egui paint callbacknix— SIGINT/SIGSTOP/SIGCONT to process groups (pause/resume/cancel jobs)image(JPEG only) — decodes upscale preview framestrash— FreeDesktop.org trash (recoverable deletes)png— decodes the embedded window iconserde+toml— settings persistence (~/.config/vhs-gui/config.toml)
- Monitor — opens the V4L2 capture device directly in mpv for a live signal check before recording. The device is released cleanly once capture starts.
- Start/Stop Capture — spawns the capture process; the archival file opens in the embedded player as soon as it appears on disk, with a rolling near-live preview window. Ctrl+C/SIGINT during capture is a normal stop, not a failure.
- Library panel — five sections in display order: Viewer → Edit Master (VD) → Edit Master → Stabilized → Archival. Click any entry to open it in the player.
- Pipeline actions — per-section buttons launch Denoise, QTGMC, IVTC, VDecimate, Viewer Encode, and all upscale variants as background jobs. Only one job runs at a time; buttons are disabled while busy.
- Upscale jobs — dual progress bars (total segments / completed; upscaled frames / extracted frames for the active segment). Pause (SIGSTOP), Resume (SIGCONT), Stop after Segment (clean SIGINT at the next segment boundary), Cancel (immediate SIGINT). A side-by-side "Original vs Upscaled" preview updates every 4 seconds while a job runs.
- Rename — Viewer files get a title-suggestion field pre-filled from the filename (strips pipeline suffixes/prefixes, title-cases with acronym preservation, formats with an em dash).
- Delete — moves files to the FreeDesktop.org trash, recoverable from any file manager.
Capture state machine: Idle → Monitoring → Releasing (1s timeout) → Capturing.
The V4L2 device is exclusive to ffmpeg during capture; mpv returns to idle during
Releasing then switches to the growing archival file once capture begins.
vhs-gui is migrating away from shelling out to bash pipeline scripts, phase by
phase, toward being fully self-contained. Each phase replaces a handful of
bash-backed buttons with native Rust that spawns the underlying tools (ffmpeg,
vspipe, Real-ESRGAN) directly — never a wrapper script. src/config.rs marks each
remaining bash-backed operation with a // TODO(native-port) comment so the
remaining surface area is always visible in the source.
- Requires the
libmpv2runtime. - System
ffmpegat/usr/bin/ffmpeg. realesrgan-rocmshim at~/bin/realesrgan-rocmfor the ROCm upscale backend. Model weights aren't bundled with this repo — grab pre-packaged archives (official + community VHS-tuned, both.pthand ncnn formats) from vhs-cli'smodels-v1release.- VapourSynth (
vspipe) +PYTHONPATHpointing at~/.local/share/vsrepo/pyfor the deinterlace/telecine operations.
