View Source Xav (xav v0.12.0)

Xav

Hex.pm API Docs CI codecov

Elixir wrapper over FFmpeg for reading audio and video files.

See an interview with FFmpeg enthusiast: https://youtu.be/9kaIXkImCAM

Installation

Make sure you have installed FFMpeg (ver. 4.x - 7.x) development packages on your system (see here for installation one-liners) and add Xav to the list of your dependencies:

def deps do
  [
    {:xav, "~> 0.12.0"},
    # Add Nx if you want to have Xav.Frame.to_nx/1
    {:nx, ">= 0.0.0"}
  ]
end

Controlling FFmpeg log output

FFmpeg's underlying libraries (libavcodec, libswscale, ...) print to stderr at the AV_LOG_INFO level by default. This is usually fine but can produce informational noise such as

[swscaler @ 0x1490a0000] No accelerated colorspace conversion found from yuv420p to rgb24.

when libswscale falls back to a generic colorspace conversion path. These are not errors — decoded frames are bit-exact — but they can clutter test output and logs.

You can raise the threshold from Elixir:

Xav.set_log_level(:error)

Or set it once at application start by configuring your application env:

# config/runtime.exs
config :xav, ffmpeg_log_level: :error

Xav.Application reads this on boot and applies it before your supervision tree starts. Valid atoms are :quiet, :panic, :fatal, :error, :warning, :info, :verbose, :debug, and :trace. An integer FFmpeg level is also accepted.

Note that av_log_set_level/1 is process-global — changing the level affects every libav* call made from the current OS process, not just the reader that triggered the change.

Usage

Decode

decoder = Xav.Decoder.new(:vp8, out_format: :rgb24)
{:ok, %Xav.Frame{} = frame} = Xav.Decoder.decode(decoder, <<"somebinary">>)

Decode with audio resampling

decoder = Xav.Decoder.new(:opus, out_format: :flt, out_sample_rate: 16_000)
{:ok, %Xav.Frame{} = frame} = Xav.Decoder.decode(decoder, <<"somebinary">>)

Read from a file:

r = Xav.Reader.new!("./some_mp4_file.mp4")
{:ok, %Xav.Frame{} = frame} = Xav.Reader.next_frame(r)
tensor = Xav.Frame.to_nx(frame)
Kino.Image.new(tensor)

Read from a camera:

r = Xav.Reader.new!("/dev/video0", device?: true, out_format: :rgb24)
{:ok, %Xav.Frame{} = frame} = Xav.Reader.next_frame(r)
tensor = Xav.Frame.to_nx(frame)
Kino.Image.new(tensor)

Speech to text:

{:ok, whisper} = Bumblebee.load_model({:hf, "openai/whisper-tiny"})
{:ok, featurizer} = Bumblebee.load_featurizer({:hf, "openai/whisper-tiny"})
{:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "openai/whisper-tiny"})
{:ok, generation_config} = Bumblebee.load_generation_config({:hf, "openai/whisper-tiny"})

serving =
  Bumblebee.Audio.speech_to_text_whisper(whisper, featurizer, tokenizer, generation_config,
    defn_options: [compiler: EXLA]
  )

# Read a couple of frames.
# See https://hexdocs.pm/bumblebee/Bumblebee.Audio.WhisperFeaturizer.html for default sampling rate.
frames =
    Xav.Reader.stream!("sample.mp3", read: :audio, out_format: :flt, out_channels: 1, out_sample_rate: 16_000)
    |> Stream.take(200)
    |> Enum.map(fn frame -> Xav.Frame.to_nx(frame) end)

batch = Nx.Batch.concatenate(frames)
batch = Nx.Defn.jit_apply(&Function.identity/1, [batch])
Nx.Serving.run(serving, batch) 

Development

To make clangd aware of the header files used in your project, you can create a compile_commands.json file. clangd uses this file to know the compiler flags, include paths, and other compilation options for each source file.

Install bear

The easiest way to generate compile_commands.json from a Makefile is to use the bear tool. bear is a tool that records the compiler calls during a build and creates the compile_commands.json file.

You can install bear with your package manager:

  • macOS: brew install bear
  • Ubuntu/Debian: sudo apt install bear
  • Fedora: sudo dnf install bear

Generate compile_commands.json

After installing bear, you can run it alongside your make command to capture the necessary information.

bear -- mix compile

Summary

Types

A human-readable FFmpeg log level.

Functions

List all decoders.

List all encoders.

Get all available pixel formats.

Get all available audio sample formats.

Sets the FFmpeg log level.

Types

@type decoder() :: %{
  codec: atom(),
  name: atom(),
  long_name: String.t(),
  media_type: atom()
}
@type encoder() :: %{
  codec: atom(),
  name: atom(),
  long_name: String.t(),
  media_type: atom(),
  profiles: [String.t()],
  sample_formats: [atom()]
}
@type log_level() ::
  :quiet
  | :panic
  | :fatal
  | :error
  | :warning
  | :info
  | :verbose
  | :debug
  | :trace

A human-readable FFmpeg log level.

The mapping to FFmpeg's AV_LOG_* constants is:

AtomFFmpeg constantValue
:quietAV_LOG_QUIET-8
:panicAV_LOG_PANIC0
:fatalAV_LOG_FATAL8
:errorAV_LOG_ERROR16
:warningAV_LOG_WARNING24
:infoAV_LOG_INFO32
:verboseAV_LOG_VERBOSE40
:debugAV_LOG_DEBUG48
:traceAV_LOG_TRACE56

FFmpeg's default is :info.

Functions

@spec list_decoders() :: [decoder()]

List all decoders.

@spec list_encoders() :: [encoder()]

List all encoders.

@spec pixel_formats() :: [{atom(), integer(), boolean()}]

Get all available pixel formats.

The result is a list of 3-element tuples {name, nb_components, hw_accelerated_format?}:

  • name - The name of the pixel format.
  • nb_components - The number of the components in the pixel format.
  • hw_accelerated_format? - Whether the pixel format is a hardware accelerated format.
@spec sample_formats() :: [{atom(), integer()}]

Get all available audio sample formats.

The result is a list of 2-element tuples {name, nb_bytes}:

  • name - The name of the sample format.
  • nb_bytes - The number of bytes per sample.
@spec set_log_level(log_level() | integer()) :: :ok

Sets the FFmpeg log level.

Accepts either one of the level atoms listed in log_level/0 or a raw integer level. Returns :ok.

This call wraps FFmpeg's av_log_set_level/1, which is process-global: the level is shared across every libav* and libswscale call in the current OS process, including readers, decoders, encoders, and converters created from the Elixir VM.

Typical use is to silence the informational [swscaler @ ...] lines that libswscale prints when it falls back to a non-SIMD colorspace conversion path (which happens for example on yuv420p -> rgb24 on Apple Silicon):

Xav.set_log_level(:error)

To configure the level declaratively at application start, use the :ffmpeg_log_level key in your application env instead:

# config/runtime.exs
config :xav, ffmpeg_log_level: :error

Xav.Application reads this on boot and calls set_log_level/1 for you.

Examples

iex> Xav.set_log_level(:error)
:ok

iex> Xav.set_log_level(:quiet)
:ok