Rules for AI coding assistants and humans using exmpeg.
Use the library, not the CLI
- Do NOT shell out to
ffmpegorffprobefrom application code. - Use
Exmpeg.probe/1instead ofSystem.cmd("ffprobe", ...). - Use
Exmpeg.remux/3for stream copy operations (container conversion, trim by time without re-encoding). - The CLI is only acceptable inside
test/support/fixtures.exfor generating synthetic test inputs - it never appears inlib/.
Always destructure results
- Every public function returns
{:ok, value}or{:error, %Exmpeg.Error{}}. - Never
Map.get/2against the error struct - pattern-match on%Exmpeg.Error{reason: reason}and branch on the reason atom. - Reasons are stable across releases. Add a new clause when the library adds a new reason; do not assume an unknown reason is fatal.
Stream metadata
Exmpeg.Stream.t/0carries:audioor:videosub-maps populated only for the matching:kind. Pattern-match on the sub-map shape rather than callingMap.get(stream.audio, :sample_rate)- the latter raisesBadMapErroron a video stream becausestream.audioisnil. The pattern-match makes the per-kind branch explicit at the call site.:duration_sisnilwhen ffmpeg cannot derive a duration. Treatnilas "unknown", not "zero".:frame_rateis{0, 1}(notnil) when unknown, matching FFmpeg's convention forAVRationaldefaults.
Remux options
:start_sdoes not seek to a keyframe. If you need precise cuts, pass a value that lands on or before a keyframe, or re-encode throughExmpeg.transcode/3.:duration_sis relative to:start_s, not absolute. Total output length is approximatelyduration_s.:drop_audio/:drop_video/:drop_subtitlesskip those stream kinds entirely. They are independent — set as many as you need.- The output container is inferred from the file extension. Mismatches
(e.g.
.mp4extension on Matroska content) surface as:unsupported.
Errors are typed - act on the reason
case Exmpeg.probe(path) do
{:ok, info} ->
handle(info)
{:error, %Exmpeg.Error{reason: :io_error, message: msg}} ->
Logger.warning("file unreadable: #{msg}")
:skip
{:error, %Exmpeg.Error{reason: :invalid_request}} ->
{:error, :bad_argument}
{:error, %Exmpeg.Error{} = err} ->
# Catch-all so a new reason (added in a future release) never
# raises `CaseClauseError` in production.
Logger.warning("probe failed: #{Exception.message(err)}")
:skip
endDo not catch :nif_panic and continue - it indicates a Rust-side bug
that should crash the calling process and reach the supervisor.
Don't add option fallbacks
The library validates options strictly:
Exmpeg.remux(input, output, banana: 1)
#=> {:error, %Exmpeg.Error{reason: :invalid_request, message: "unknown option :banana"}}If a feature flag is missing, file an issue or add it - do not swallow the error and substitute a default.
Transcode options
:video_codec/:audio_codectake encoder short names. The precompiled (LGPL) binaries ship"libvpx-vp9","aac","libopus","libmp3lame", and"flac". The GPL H.264 / H.265 encoders ("libx264","libx265") are not in the precompiled binaries and return:unsupportedthere; build from source (EXMPEG_BUILD=1) against a GPL-enabled FFmpeg 8 to use them."copy"(or an omitted option) stream-copies that media type without re-encoding.:video_filteraccepts an FFmpeg filter-graph spec ("scale=720:-2,fps=30,crop=in_w:in_h-100:0:50"). Setting it overrides the convenience options:width/:height/:fps.- Audio resampling uses an internal
AVAudioFifo, so codecs with a fixedframe_size(AAC, Opus, MP3) accept any input chunk size. - Mismatched codec / container combinations surface as
:unsupported.
Audio extraction container support
Exmpeg.extract_audio/3 writes the container that matches the output
extension and picks the matching encoder automatically:
| Extension | Encoder | Notes |
|---|---|---|
.wav | pcm_s16le | Default; rawest interop fit. |
.mp3 | libmp3lame | Lossy, widest playback support. |
.m4a / .aac | aac | AAC-LC in an MP4 audio container. |
.opus / .ogg | libopus | Modern lossy. |
.flac | flac | Lossless. |
The build of FFmpeg the NIF is linked against must include the
matching encoder. Missing encoders surface as :unsupported.
Features intentionally not in v0.1
The library does not currently ship:
- Subtitle burn-in / extraction beyond stream-copy.
- HLS / DASH segment muxers.
- Hardware-accelerated device init (
-hwaccel,-hwaccel_device). Hardware encoders (h264_nvenc,h264_vaapi) are still selectable by codec name through:video_codec, but only when the FFmpeg build initialises the device implicitly.
If you need one of these, file an issue with the use case rather than shelling out to the CLI as a workaround.
Memory inputs and progress (already shipped in v0.1)
Every read-side op (probe/1, extract_frame/3, extract_audio/3,
remux/3, concat/3, transcode/3) accepts {:memory, binary} in
place of a path. remux/3, extract_audio/3, concat/3, and
transcode/3 accept :progress => pid() and send throttled
{:exmpeg_progress, %{...}} messages to the pid. Wrap the call in
Task.async/1 if you want to receive those messages while the NIF
runs.