FFmpReg: a Rust media toolkit built without FFmpeg
An experimental safe multimedia toolkit
At a glance
- What is it?
- FFmpReg is an experimental Rust library and command line tool that decodes, transforms and encodes audio and video with no FFmpeg dependency, built on an explicit demux, decode, transform, encode, mux pipeline. It suits a Rust developer who wants media processing in-process without shipping a binary dependency, and its format coverage today is narrow.
- Who is it for?
- FFmpReg fits a Rust project that needs to process media in process, where linking FFmpeg and shipping its shared libraries is the part you want to avoid, and where the pipeline model of explicit containers, codecs and transforms is what you want to reason about. Skip it if you need broad codec coverage, since the formats today are WAV and Y4M with PCM and ADPCM audio, which is a research scope rather than a production one.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 160 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The premise is a Rust native alternative to shelling out
FFmpReg is a rust-native multimedia toolkit that decodes, transforms, and encodes audio and video without requiring FFmpeg. It provides both a command-line interface and a library API, allowing you to process media files directly from the terminal or integrate media processing into Rust applications. The project aims to deliver a safe, deterministic, and modular media processing engine with a focus on explicit pipelines and developer control.
The name is a pun and it is the whole pitch. The name plays on FFmpeg, and the point of not requiring FFmpeg is that the whole pipeline is written in Rust, with no external media library to link. A Rust application that needs to normalise a WAV file does not want to ship libavcodec and deal with its version matrix.
The two delivery modes are the important structural choice. The command line tool is for use from a terminal, and the library is for embedding in a Rust application, and the documentation is explicit that the library exposes the same primitives used internally by the CLI. That is the right arrangement, since a tool that only works as a subprocess is not really usable from inside an application, and a library that only exposes its own wrapper types is not reusable.
The stated goals are worth quoting because they set the tone of the whole project. Safe and deterministic point at the absence of undefined behaviour and at reproducible output, and modular points at the pipeline design where the pieces compose.
The licensing is deliberately permissive. The project is released under the Apache 2.0 License, free for any use including commercial, and the documentation gives the reason plainly: a permissive licence helps the project grow and be useful for everyone. The sponsoring section is frank about the cost, saying the project takes a huge amount of work studying codec algorithms and implementing them from scratch, and that support covers internet, energy and tools and keeps the author coding full time for the community.
Packet and frame, and the pipeline between them
Media processing revolves around two main data types, and the split between them is the concept you need to understand before reading any of the code.
A `Packet` represents encoded data in a container, while a `Frame` represents decoded data ready for processing or playback. The flow is predictable: readers demux containers into packets, decoders convert packets into frames, transforms modify frames, encoders convert frames back into packets, and writers mux packets into output files.
The two structures are short and worth reading side by side:
pub struct Frame {
pub data: Vec<u8>,
pub pts: i64,
pub timebase: Timebase,
pub sample_rate: u32,
pub channels: u8,
pub nb_samples: usize,
}pub struct Packet {
pub data: Vec<u8>,
pub pts: i64,
pub dts: i64,
pub timebase: Timebase,
pub stream_index: usize,
}The difference in the fields is the difference in what the two layers know. A frame carries the audio shape, the sample rate, the channel count and the number of samples, because a decoded frame is where those facts are meaningful. A packet carries a decode timestamp and a stream index, because at the container level the packet is an opaque blob belonging to one stream, and the timing is a property of the container's timeline rather than of the samples.
The `Timebase` appears in both, and that is the mechanism that keeps them aligned. A presentation timestamp in the packet's timebase has to be converted into the frame's timebase, and that conversion is where a lot of real world media bugs live.
The library example makes the flow concrete, reading packets from a WAV file, decoding them into frames, optionally applying transforms, encoding the frames and writing them to an output container, and it is written as a plain loop over `reader.read_packet()` with a decoder, an encoder and a writer. The example is short enough to read in one sitting, which is the point of a reference implementation.
Transforms are a trait, and the CLI exposes them as strings
Transforms modify frames in the pipeline, and the built-in ones are `Gain` for multiplying sample amplitudes and `Normalize` for scaling samples to a target peak.
The trait is small, and the custom transform example is a good one to read because it shows the contract:
pub struct Invert;
impl Transform for Invert {
fn apply(&mut self, mut frame: Frame) -> Result<Frame> {
for i in (0..frame.data.len()).step_by(2) {
let sample = i16::from_le_bytes([frame.data[i], frame.data[i + 1]]);
let inverted = (-sample).to_le_bytes();
frame.data[i] = inverted[0];
frame.data[i + 1] = inverted[1];
}
Ok(frame)
}
fn name(&self) -> &'static str {
"invert"
}
}So a transform is anything that takes a frame and returns a frame, plus a name. The name is what makes the CLI side work, and it is a small design decision with a large payoff.
CLI transforms are specified as name=value pairs:
ffmpreg -i input.wav -o output.wav --apply gain=1.5 --apply normalize=0.9And the order matters, because transforms execute in the order they appear. That is why the CLI takes repeated `--apply` rather than a list, since a list would suggest an unordered set. The same ordering rule holds in the library, where the example applies gain then normalize to successive frames.
The trait is also why the pipeline is extensible without the library knowing about your processing. The invert example is deliberately a bit of a toy, inverting signed 16-bit samples, but it demonstrates that you can reach into `frame.data` directly and treat it as raw sample bytes, which is what you would do for a filter that has no built-in transform.
Note that the `Normalize` transform takes a target peak, so `normalize=0.9` means scale to 0.9 of full scale, while `normalize` with no value uses the default. That distinction between a bare name and a name with a parameter is how the CLI carries the arguments of the built-in transforms.
The format coverage today is two containers and three codecs
The formats section is short, and it is the section that decides whether the project is useful to you right now.
The formats are WAV, which is uncompressed audio and suitable for lossless pipelines, and Y4M, which is raw video frames with a text-based header.
The codecs are PCM, which is uncompressed 16-bit audio with multi-channel support, ADPCM, which is adaptive differential audio compression, and raw video, which passes through YUV frame data unchanged.
So the current state is uncompressed or lightly compressed audio and uncompressed video. That is a deliberate shape for a project whose stated purpose is studying codec algorithms and implementing them from scratch, but it means the tool cannot yet do the thing people usually want from a media tool, which is transcode an MP4 or an AAC file into something smaller.
What the project does have is the plumbing, and the plumbing is the reusable part. Container parsing, packet and frame types, timebase handling, a transform trait, an encoder and decoder interface, and a muxing layer. The video path follows the same pipeline as the audio path, with a `Y4mReader` and `Y4mWriter` for the container and `RawVideoDecoder` and `RawVideoEncoder` for the codec, and the same reader, decoder, transform, encoder, writer chain.
The format metadata is reachable through the reader:
let format = reader.format();
println!("Resolution: {}x{}", format.width, format.height);
println!("Frame rate: {}/{}", format.framerate_num, format.framerate_den);So the metadata layer is already in place for video, waiting on a codec that produces more than raw YUV.
The inspection mode is the other place the current scope shows. The `--show` flag prints frame-level metadata similar to ffprobe, including timing, sample count, channel configuration and sample rate, and the sample output shows one line per frame with the presentation timestamp, the sample count, the channels and the rate.
The CLI reference is correspondingly short. `-i` is the input file or glob pattern, `-o` is the output file or directory, `--show` is inspection mode, `--apply` adds a transform and may be given multiple times, and `--codec` selects the output codec with the default matching the input.
The build profiles say more about the project than the features do
The Cargo manifest has a set of profiles that is unusual enough to be worth reading closely, because it is a statement about how the author works.
The release profile uses `opt-level = 3`, `lto = "fat"` and `codegen-units = 1`, which is the maximal optimisation setting, and `strip = "symbols"` with `debug = false`. The `panic = "abort"` line carries a comment that is the most revealing thing in the manifest: let it crash and force ourselves to write safe Rust.
That is a strong statement, and a defensible one for a library that converts untrusted media into typed structures. A panic in a decoder on a malformed file is a crash rather than a caught error, and the author has decided that the discipline of writing code that cannot panic is preferable to sprinkling unwinding paths through a parser.
The dev profile sets `debug = false` with the same reasoning repeated: disabling debug info speeds up local and CI builds, and we don't rely on it for debugging that much.
Then there are two extra profiles, and they exist for good reasons. A `release-with-debug` profile inherits release but keeps the debug information, used with `cargo build --profile=release-with-debug` for profiling and debugging. And a `coverage` profile inherits release but drops to `opt-level = 2` and `codegen-units = 256` with thin LTO so it compiles faster, while turning on `debug-assertions`, `overflow-checks` and `debug-assert!`s so that arithmetic overflow is caught and the debug assertions pass during the coverage run.
The dependencies are lean and mostly about parsing and CLI rather than media. `clap` with the derive feature is the command line, `nom` at 8.0.0 is the parser combinator library, `serde` and `serde_json` for serialisation, `rustc-hash` for hashing, and `reqwest` with blocking and default-tls, which is the HTTP client, presumably for fetching a codec or model over the network. There is no image or media crate in the list at all, which is consistent with the codecs being implemented from scratch.
The repository layout is small and conventional. There is `src/`, `tests/`, `examples/` with `api.rs`, `api_container.rs` and `metadata.rs`, a `run.sh`, a `rustfmt.toml`, and the usual metadata files. The version is 0.2.0 on edition 2024, and the documentation says the project uses MAJOR.MINOR.PATCH, with MAJOR for incompatible API changes or major new features, MINOR for new features compatible with existing APIs and PATCH for bug fixes.
Editorial conclusion
FFmpReg fits a Rust project that needs to process media in process, where linking FFmpeg and shipping its shared libraries is the part you want to avoid, and where the pipeline model of explicit containers, codecs and transforms is what you want to reason about. Skip it if you need broad codec coverage, since the formats today are WAV and Y4M with PCM and ADPCM audio, which is a research scope rather than a production one. Before you plan around it, read the version status, because the crate is at 0.2.0 and the documentation describes pre-1.0 releases as functionally unstable. The last push was on 2026-05-16.
Frequently asked questions
What is FFmpReg?
It is a rust-native multimedia toolkit that decodes, transforms and encodes audio and video without requiring FFmpeg, providing both a command-line interface and a library API, aiming at a safe, deterministic and modular media processing engine.
How do I install ffmpreg?
Run `cargo install ffmpreg`, which downloads and compiles it and places the binary in your Cargo bin directory. To use it as a library, add `ffmpreg = "0.1"` to the dependencies in your Cargo.toml.
Which formats and codecs does FFmpReg support?
The formats are WAV for uncompressed audio and Y4M for raw video frames with a text-based header, and the codecs are PCM uncompressed 16-bit audio, ADPCM, and raw video which passes through YUV data unchanged.
How do I write a custom transform in FFmpReg?
Implement the Transform trait, which requires an apply method taking a Frame and returning a Result<Frame>, and a name method. The name is what lets the CLI address it as a name=value pair after an --apply flag.
Is FFmpReg stable enough to depend on?
The crate is at version 0.2.0 and the documentation notes that pre-1.0 releases are functionally unstable, so pin an exact version. The licence is Apache 2.0 and the last push was on 2026-05-16.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/yazaldefilimone-ffmpreg)Was this useful?