Native Elixir bindings for FFmpeg via the rsmpeg Rust crate.
This library replaces shelling out to the ffmpeg / ffprobe CLIs with an
in-process 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
# 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.rsquarantines the small number of operations rsmpeg does not yet expose safely:- clearing
AVCodecParameters.codec_tag(single primitive store on a unique&mutborrow), AVAudioFifo::write/AVAudioFifo::readagainst a frame'sextended_dataper-channel pointer array,- assigning a freshly-built
AVDictionaryintoAVFormatContextOutput.metadata(libavformat takes ownership). Everyunsafeblock carries aSAFETY:comment naming the invariant; unit tests in the same module exercise the round-trips.
- clearing
native/exmpeg_native/src/progress.rsreconstructs anEnv<'_>from the rawNIF_ENVcaptured at the entry point so that long-running ops can emit throttled{:exmpeg_progress, ...}messages without anOwnedEnv(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
def deps do
[
{:exmpeg, "~> 0.1"}
]
endThe 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.
rsmpegdiscovers them viapkg-config; setFFMPEG_PKG_CONFIG_PATHwhen 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:
libc2.35+ (Ubuntu 24.04 / Debian 12) withlibm,libdl,libpthread. Standard on any modern Linux.- The codec system libraries that libavcodec dlopens at decode/encode
time:
libmp3lame(libmp3lame0)libopus(libopus0)libvpx(libvpx9or newer)libwebp(libwebp7or newer) - for.webpframe output
- Their transitive system deps (
libgsm,libnuma, ...) which the distro packages above pull in automatically.
For Debian / Ubuntu:
sudo apt install -y libmp3lame0 libopus0 libvpx9 libwebp7
For macOS (Apple Silicon, via Homebrew):
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{}}.
Exmpeg.Error.reason/0 enumerates the categories: :invalid_request,
:io_error, :decode_error, :encode_error, :unsupported,
:runtime_error, :nif_panic, :native_error.
Development
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:
task test:integration
License
MIT. See LICENSE.