A minimal, terminal-based video segment cutter written in Rust. It uses ffmpeg for processing and a custom differential renderer (with native Kitty graphics protocol support) for in-terminal video playback.
This project’s code is under the MIT license.
I had a bunch of NVIDIA shadowplay clips sitting around and needed a quicker way to do simple tasks like cutting out the important clip from 5-minute, multi-GB files. Fed up with the tedious import, mark, cut, export process with software like OBS or the inbuilt Windows editor (which would hang for multiple minutes on exporting), I built this.
- License
- Motivation
- Features
- Requirements
- Building
- Usage
- Keybinds
- Demo Recording
- Roadmap
- Architecture & Concurrency
- Benchmarks
- In-Terminal Playback: Uses a custom, high-performance differential ANSI renderer for default low-res playback, with native Kitty graphics protocol support for high-res rendering without heavy external dependencies.
- Two Display Modes:
- Low-Res: Fast, block-based rendering that works in most terminals.
- High-Res: Pixel-based rendering using Kitty or iTerm graphics protocols (if supported).
- Audio Preview & Volume Control: Real-time audio playback through default output device using
rodioandffmpeg. Adjust editor playback volume (+/-) and export volume (a/A) independently. - Segment-Based Editing:
- Place markers (
v) to define segments. - Toggle segments (
t) for inclusion or exclusion in the final export.
- Place markers (
- Interactive Timeline: A simple timeline shows the playhead, markers, and included/excluded segments.
- Dual Output Modes:
- Multi-File: Exports each "included" segment as a separate video file.
- Single-File: Concatenates all "included" segments into one continuous video file.
- Non-Destructive: All operations are non-destructive. Your original video file is never modified.
Warning
- It has been tested on Linux and macOS, and is untested on Windows.
- It may not build or run on Windows without manual dependency setup (
ffmpeg,ffprobe,rodio). - Terminal support for high-resolution playback (Kitty graphics) is auto-detected or enabled via
-r/--high-res. Low-res mode is the default.
Before you begin, you must have two external dependencies installed and available in your system's PATH:
- ffmpeg: Used for all video decoding, seeking, and segment exporting.
- ffprobe: Used to get video metadata (dimensions, duration, FPS).
You also need the Rust toolchain (e.g., rustc and cargo) to build the project.
-
Clone the repository:
git clone https://github.com/pbossev/rve.git cd rve -
Build the release binary:
cargo build --release
-
The final binary will be located at
target/release/rve. You can copy this to a directory in yourPATH:sudo cp target/release/rve /usr/local/bin/
The binary is named rve. To run it, simply pass a video file as an argument:
rve /path/to/my_video.mp4Based on the --help menu:
<filepath>: (Required) The path to the video file you want to edit.--single-output,-s: On exit, concatenate all "included" segments into one file (e.g.,filename_concat.mp4). If omitted, it defaults to multi-file mode.--high-res,-r: Start the application in high-resolution pixel mode using the native Kitty graphics protocol (supported in Kitty, WezTerm, Ghostty, etc.).--help,-h: Show the help menu.
| Key | Action |
|---|---|
Space |
Play / Pause |
. |
Next frame (when paused) |
, |
Previous frame (when paused) |
< / > |
Decrease / Increase playback speed (0.25x - 2.0x) |
+ / - |
Increase / Decrease in-editor playback volume |
| Key | Action |
|---|---|
← / → |
Seek -5s / +5s |
Alt+← / Alt+→ |
Seek -30s / +30s |
Ctrl+← / Ctrl+→ |
Seek -60s / +60s |
0-9 |
Jump to 0% - 90% of the video |
| Key | Action |
|---|---|
v |
Place or remove a marker at the current playhead. |
t |
Toggle the current segment (between markers) as "Included" or "Excluded". |
[ |
Jump to the previous marker. |
] |
Jump to the next marker. |
a / A |
Increase / Decrease export audio volume |
| Key | Action |
|---|---|
r |
Toggle between Low-Res (block) and High-Res (pixel) display mode (only on compatible terminals). |
i |
Toggle output mode between "Multi-File" and "Single-File". |
? |
Toggle the on-screen keybinding help display. |
s |
Save/output. Suspends UI and begins the ffmpeg export process based on your segments. |
q / Esc |
Quit. Prompts for confirmation before exiting without saving. |
A VHS tape file is included at demo/demo.tape to record terminal demonstrations of RVE automatically.
To generate the animated demo GIF, WebM recording, and step-by-step screenshots:
- Install VHS: Ensure
vhs(and its dependenciesffmpegandttyd) is installed. - Build the binary: Compile RVE with the
--releaseflag, so the binary exists in the target directory. - Sample video: Ensure
test_video.mp4is present in thedemo/directory. - Run VHS:
vhs demo/demo.tape
This executes the automated interaction defined in demo/demo.tape and saves the generated media into demo/out/:
demo/out/demo.gif– Animated GIF of the terminal sessiondemo/out/demo.webm– WebM video recordingdemo/out/screenshot_1.png–screenshot_10.png– Step-by-step frame screenshots
I plan to develop this more as I get more time for it.
- Add basic audio preview support.
- Add basic audio volume editing/control support.
- Add multi-file importing.
- More advanced editing features (e.g., re-ordering segments, changing playback speed).
RVE is designed to be as non-blocking and fast as possible when dealing with video data. It relies on standard standard OS pipes to communicate with ffmpeg sub-processes, with no heavy C-bindings.
graph TD
A[Main Event Loop] -->|Polls Events| B(Update State)
B -->|Fetch Frame| C[Frame Iterator]
B -->|Audio Sync/Vol| L[Audio Player]
subgraph Video Decoding
C -->|Reads Pipe| D(BufReader)
D ---|Stdout| E[ffmpeg -f rawvideo]
end
subgraph Audio Playback
L -->|Manages| M(Audio Thread)
M -->|Reads Pipe| N[ffmpeg -f s16le]
M -->|Plays PCM| O[rodio Sink]
end
A -->|Renders UI| F(Terminal View)
subgraph Parallel Export
B -->|On Save| G[process_final_output]
G -->|Spawns Thread 1| H[ffmpeg segment 1]
G -->|Spawns Thread 2| I[ffmpeg segment 2]
G -->|Spawns Thread N| J[ffmpeg segment N]
H -.->|Join| K(Concat Process if Single-File)
I -.->|Join| K
J -.->|Join| K
end
- Frame Decoding: When a video is opened,
rvespawns anffmpegprocess that decodes the video into raw RGB24 frames.FrameIteratorreads these frames directly into a pre-allocated pixel buffer usingstd::mem::replaceto ensure zero-allocation per frame, achieving high throughput. - Parallel Export: When you save,
rvespawns a native OS thread for each segment you want to include. Each thread launches anffmpeg -c copysubprocess. This allows multiple segments to be extracted from the source video concurrently, drastically speeding up the export.
RVE uses Criterion.rs to monitor performance regressions.
frame_pipeline: Measures the raw throughput of pulling frames through theBufReaderand constructingimage::RgbImagestructs. By reusing buffers, the overhead of the Rust pipeline is practically zero, bounded entirely byffmpeg's decode speed.export: Compares the speed of sequential vs parallel exporting. Due to the parallel threading architecture, exporting multiple segments concurrently yields significant speedups (e.g., a ~3.3x speedup when exporting 4 segments on modern hardware) sinceffmpegcopy operations are largely I/O and stream-parse bound.renderer: Compares the custom differential terminal renderer against the nativekitty_renderer. The differential renderer tracks frame states and only writes ANSI escape codes for the exact pixels that change. This cuts down terminal I/O overhead during static or low-motion scenes.
| Benchmark | Test | Time (avg) | Throughput | Notes |
|---|---|---|---|---|
segment_export |
sequential |
~399.65 ms | - | 4 segments of 5 seconds each |
segment_export |
parallel |
~115.80 ms | - | ~3.45x speedup over sequential |
frame_decode |
take_frame_lowres |
~40.23 ms | ~24.85 fps | rawvideo pipe decode overhead |
renderers |
differential_0_percent |
~33.58 µs | - | 0% frame change (static scene) |
renderers |
differential_10_percent |
~108.13 µs | - | 10% frame change |
renderers |
differential_100_percent |
~690.87 µs | - | 100% frame change (full redraw) |
renderers |
kitty_renderer |
~692.38 µs | - | Native Kitty graphics renderer |
| Benchmark | Test | Time (avg) | Throughput | Notes |
|---|---|---|---|---|
segment_export |
sequential |
~191.23 ms | - | 4 segments of 5 seconds each |
segment_export |
parallel |
~52.84 ms | - | ~3.62x speedup over sequential |
frame_decode |
take_frame_lowres |
~7.85 ms | ~127.40 fps | rawvideo pipe decode overhead |
renderers |
differential_0_percent |
~20.64 µs | - | 0% frame change (static scene) |
renderers |
differential_10_percent |
~59.12 µs | - | 10% frame change |
renderers |
differential_100_percent |
~391.15 µs | - | 100% frame change (full redraw) |
renderers |
kitty_renderer |
~401.07 µs | - | Native Kitty graphics renderer |
Note on kitty_renderer: The benchmark uses gag to intercept and suppress terminal stdout. In the real application, Kitty graphics transfers uncompressed RGB data to the terminal emulator.
You can run the benchmarks yourself with:
cargo bench