Rules for AI coding assistants and humans using exmpeg.

Use the library, not the CLI

  • Do NOT shell out to ffmpeg or ffprobe from application code.
  • Use Exmpeg.probe/1 instead of System.cmd("ffprobe", ...).
  • Use Exmpeg.remux/3 for stream copy operations (container conversion, trim by time without re-encoding).
  • The CLI is only acceptable inside test/support/fixtures.ex for generating synthetic test inputs - it never appears in lib/.

Always destructure results

  • Every public function returns {:ok, value} or {:error, %Exmpeg.Error{}}.
  • Never Map.get/2 against 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/0 carries :audio or :video sub-maps populated only for the matching :kind. Pattern-match on the sub-map shape rather than calling Map.get(stream.audio, :sample_rate) - the latter raises BadMapError on a video stream because stream.audio is nil. The pattern-match makes the per-kind branch explicit at the call site.
  • :duration_s is nil when ffmpeg cannot derive a duration. Treat nil as "unknown", not "zero".
  • :frame_rate is {0, 1} (not nil) when unknown, matching FFmpeg's convention for AVRational defaults.

Remux options

  • :start_s does not seek to a keyframe. If you need precise cuts, pass a value that lands on or before a keyframe, or re-encode through Exmpeg.transcode/3.
  • :duration_s is relative to :start_s, not absolute. Total output length is approximately duration_s.
  • :drop_audio / :drop_video / :drop_subtitles skip 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. .mp4 extension 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
end

Do 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_codec take 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 :unsupported there; 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_filter accepts 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 fixed frame_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:

ExtensionEncoderNotes
.wavpcm_s16leDefault; rawest interop fit.
.mp3libmp3lameLossy, widest playback support.
.m4a / .aacaacAAC-LC in an MP4 audio container.
.opus / .ogglibopusModern lossy.
.flacflacLossless.

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.