# exmpeg
Native Elixir bindings for FFmpeg via the [`rsmpeg`](https://crates.io/crates/rsmpeg) Rust crate.
This library replaces shelling out to the `ffmpeg` / `ffprobe` CLIs with an
in-process [Rustler](https://github.com/rusterlium/rustler) NIF. Every call
runs against the FFmpeg shared libraries the NIF was linked at compile time
and returns structured results as plain Elixir structs / maps.
## Status
`v0.1` covers the operations needed to fully replace the `ffmpeg` /
`ffprobe` CLI for the common cases:
| Operation | Replaces |
| -------------------------- | ----------------------------------------------------------------- |
| `Exmpeg.probe/1` | `ffprobe -show_format -show_streams` |
| `Exmpeg.remux/3` | `ffmpeg -i in -c copy out` (with optional `-ss` / `-t` cut) |
| `Exmpeg.extract_frame/3` | `ffmpeg -ss T -i in -frames:v 1 out.jpg` |
| `Exmpeg.extract_audio/3` | `ffmpeg -i in -vn -acodec pcm_s16le out.wav` |
| `Exmpeg.concat/2` | `ffmpeg -f concat -i list.txt -c copy out` |
| `Exmpeg.transcode/3` | `ffmpeg -i in -c:v libvpx-vp9 -c:a libopus out` (and friends) |
## Quickstart
```elixir
# Probe (ffprobe)
{:ok, info} = Exmpeg.probe("input.mkv")
info.format.duration_s
#=> 12.345
# Remux: container change, optional cut window
{:ok, _} = Exmpeg.remux("input.mkv", "output.mp4")
{:ok, _} = Exmpeg.remux("input.mp4", "clip.mp4", start_s: 5.0, duration_s: 2.0)
# Thumbnail at a timestamp, optionally resized
{:ok, _} = Exmpeg.extract_frame("input.mp4", "thumb.jpg", timestamp_s: 1.5, width: 320)
# Audio to WAV with explicit sample rate + channels
{:ok, _} = Exmpeg.extract_audio("input.mp4", "audio.wav", sample_rate: 16_000, channels: 1)
# Concat three same-codec clips
{:ok, _} = Exmpeg.concat(["a.mp4", "b.mp4", "c.mp4"], "joined.mp4")
# Re-encode to VP9 + Opus at a smaller width / lower audio rate.
# (The precompiled binaries are LGPL: VP9/Opus/MP3/AAC/FLAC work
# out of the box; H.264 via libx264 needs a GPL source build.)
{:ok, _} =
Exmpeg.transcode("input.mov", "output.webm",
video_codec: "libvpx-vp9", audio_codec: "libopus",
width: 1280, sample_rate: 48_000
)
```
## Safety
The Rust crate is built on rsmpeg's safe wrappers with
`#![deny(unsafe_code)]` at the root. Two modules contain `unsafe`
blocks; every other module is `unsafe`-free.
- `native/exmpeg_native/src/ffi_helpers.rs` quarantines the small
number of operations rsmpeg does not yet expose safely:
- clearing `AVCodecParameters.codec_tag` (single primitive store
on a unique `&mut` borrow),
- `AVAudioFifo::write` / `AVAudioFifo::read` against a frame's
`extended_data` per-channel pointer array,
- assigning a freshly-built `AVDictionary` into
`AVFormatContextOutput.metadata` (libavformat takes ownership).
Every `unsafe` block carries a `SAFETY:` comment naming the
invariant; unit tests in the same module exercise the round-trips.
- `native/exmpeg_native/src/progress.rs` reconstructs an `Env<'_>`
from the raw `NIF_ENV` captured at the entry point so that
long-running ops can emit throttled `{:exmpeg_progress, ...}`
messages without an `OwnedEnv` (which panics on dirty-scheduler
threads). The captured pointer is valid for the lifetime of the
enclosing NIF call and the emitter cannot outlive that call.
Every NIF entry point is wrapped in `run_with_panic_protection`, so a
Rust panic surfaces as `{:error, %{type: "nif_panic", ...}}` instead
of taking down the BEAM VM.
## Installation
```elixir
def deps do
[
{:exmpeg, "~> 0.1"}
]
end
```
The published Hex package ships precompiled NIFs for common targets
(`aarch64-apple-darwin`, `x86_64-unknown-linux-gnu`,
`aarch64-unknown-linux-gnu`); consumers do not need a Rust toolchain to
use them.
To build the NIF from source, install Rust 1.85 or newer and set
`EXMPEG_BUILD=1` before compiling.
## Build requirements
- FFmpeg 8.x shared libraries on the linker / loader path. `rsmpeg`
discovers them via `pkg-config`; set `FFMPEG_PKG_CONFIG_PATH` when
building against a non-default install.
- Rust 1.85+ for source builds.
- Elixir 1.17+ / OTP 26+.
## Runtime requirements (precompiled NIF consumers)
The published Hex package ships precompiled NIF tarballs that **bundle
the six FFmpeg shared libraries** (`libavformat`, `libavcodec`,
`libavutil`, `libavfilter`, `libswscale`, `libswresample`) next to the
NIF and use `$ORIGIN` / `@loader_path` so the loader finds them without
`LD_LIBRARY_PATH` gymnastics. Consumers therefore do **not** need to
install FFmpeg 8 separately.
The bundled FFmpeg is built **LGPL-only** (`--enable-libmp3lame
--enable-libopus --enable-libvpx`, no `--enable-gpl`), so the precompiled
binaries can be redistributed under this package's MIT license.
H.264 / H.265 software encoding via `libx264` / `libx265` is GPL and is
**not** in the precompiled binaries; calling `transcode/3` with
`video_codec: "libx264"` (or `"libx265"`) on a precompiled install
returns `{:error, %Error{reason: :unsupported}}`. To use them, build
from source (`EXMPEG_BUILD=1`) against your own GPL-enabled FFmpeg 8.
What is **not** bundled and must be on the host:
- `libc` 2.35+ (Ubuntu 24.04 / Debian 12) with `libm`, `libdl`,
`libpthread`. Standard on any modern Linux.
- The codec system libraries that libavcodec dlopens at decode/encode
time:
- `libmp3lame` (`libmp3lame0`)
- `libopus` (`libopus0`)
- `libvpx` (`libvpx9` or newer)
- `libwebp` (`libwebp7` or newer) - for `.webp` frame output
- Their transitive system deps (`libgsm`, `libnuma`, ...) which the
distro packages above pull in automatically.
For Debian / Ubuntu:
```bash
sudo apt install -y libmp3lame0 libopus0 libvpx9 libwebp7
```
For macOS (Apple Silicon, via Homebrew):
```bash
brew install lame opus libvpx webp
```
Source builds (`EXMPEG_BUILD=1`) link directly against the system's
FFmpeg 8 install and so behave like a normal `pkg-config` consumer:
they need the dev packages (`libavcodec-dev` & friends) at build time
and the matching runtime libs at load time.
## Errors
Every call returns either `{:ok, value}` or `{:error, %Exmpeg.Error{}}`.
`t:Exmpeg.Error.reason/0` enumerates the categories: `:invalid_request`,
`:io_error`, `:decode_error`, `:encode_error`, `:unsupported`,
`:runtime_error`, `:nif_panic`, `:native_error`.
## Development
```bash
task setup # mix deps.get
task compile # build the NIF (first run takes several minutes)
task test # fast Elixir unit tests
task test:rust # cargo test
task lint # mix credo --strict + cargo clippy -D warnings
task check # full local gate
```
Run integration tests (synthesises a small MP4 via the `ffmpeg` CLI) with:
```bash
task test:integration
```
## License
MIT. See [LICENSE](LICENSE).