exmpeg

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/1ffprobe -show_format -show_streams
Exmpeg.remux/3ffmpeg -i in -c copy out (with optional -ss / -t cut)
Exmpeg.extract_frame/3ffmpeg -ss T -i in -frames:v 1 out.jpg
Exmpeg.extract_audio/3ffmpeg -i in -vn -acodec pcm_s16le out.wav
Exmpeg.concat/2ffmpeg -f concat -i list.txt -c copy out
Exmpeg.transcode/3ffmpeg -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.

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"}
  ]
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

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:

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{}}. t: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.