Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ def deps do
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:

```elixir
Xav.Reader.set_log_level(:error)
```

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

```elixir
# config/runtime.exs
config :xav, 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
Expand Down
28 changes: 25 additions & 3 deletions c_src/xav/xav_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,31 @@ void free_xav_reader(ErlNifEnv *env, void *obj) {
}
}

static ErlNifFunc xav_funcs[] = {{"new", 9, new},
{"next_frame", 1, next_frame, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"seek", 2, seek, ERL_NIF_DIRTY_JOB_CPU_BOUND}};
/* Wraps av_log_set_level(int). The level integer is validated on the
* Elixir side (Xav.Reader.set_log_level/1 maps atoms to the AV_LOG_*
* constants) so the NIF only needs to pass it through. Note that
* av_log_set_level() is process-global: it changes the log level for
* every libav* call made from the current OS process, not just the
* reader that triggered the call. */
ERL_NIF_TERM set_log_level(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 1) {
return xav_nif_raise(env, "invalid_arg_count");
}

int level;
if (!enif_get_int(env, argv[0], &level)) {
return xav_nif_raise(env, "invalid_log_level");
}

av_log_set_level(level);
return enif_make_atom(env, "ok");
}

static ErlNifFunc xav_funcs[] = {
{"new", 9, new},
{"next_frame", 1, next_frame, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"seek", 2, seek, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"set_log_level", 1, set_log_level}};

static int load(ErlNifEnv *env, void **priv, ERL_NIF_TERM load_info) {

Expand Down
23 changes: 23 additions & 0 deletions lib/xav/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Xav.Application do
@moduledoc false

use Application

@impl true
def start(_type, _args) do
maybe_set_log_level()

Supervisor.start_link([], strategy: :one_for_one, name: Xav.Supervisor)
end

# Applies the `:xav, :log_level` application env value, if set.
# Without this, FFmpeg's default log level (`AV_LOG_INFO`)
# remains unchanged, preserving existing behaviour for users who
# don't opt in.
defp maybe_set_log_level do
case Application.get_env(:xav, :log_level) do
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the name log_level confusing, it may be interpreted as setting the Elixir log level. What about ffmpeg_log_level ?

nil -> :ok
level -> Xav.Reader.set_log_level(level)
end
end
end
96 changes: 96 additions & 0 deletions lib/xav/reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,102 @@ defmodule Xav.Reader do
Xav.Reader.NIF.seek(ref, time_in_seconds)
end

@typedoc """
A human-readable FFmpeg log level.

The mapping to FFmpeg's `AV_LOG_*` constants is:

| Atom | FFmpeg constant | Value |
|-------------|-------------------|-------|
| `:quiet` | `AV_LOG_QUIET` | -8 |
| `:panic` | `AV_LOG_PANIC` | 0 |
| `:fatal` | `AV_LOG_FATAL` | 8 |
| `:error` | `AV_LOG_ERROR` | 16 |
| `:warning` | `AV_LOG_WARNING` | 24 |
| `:info` | `AV_LOG_INFO` | 32 |
| `:verbose` | `AV_LOG_VERBOSE` | 40 |
| `:debug` | `AV_LOG_DEBUG` | 48 |
| `:trace` | `AV_LOG_TRACE` | 56 |

FFmpeg's default is `:info`.
"""
@type log_level ::
:quiet
| :panic
| :fatal
| :error
| :warning
| :info
| :verbose
| :debug
| :trace

@log_levels %{
quiet: -8,
panic: 0,
fatal: 8,
error: 16,
warning: 24,
info: 32,
verbose: 40,
debug: 48,
trace: 56
}

@doc """
Sets the FFmpeg log level.

Accepts either one of the level atoms listed in `t: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 elsewhere in the Elixir
VM. It is not scoped to a particular `Xav.Reader`.

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.Reader.set_log_level(:error)

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

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

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

## Examples

iex> Xav.Reader.set_log_level(:error)
:ok

iex> Xav.Reader.set_log_level(:quiet)
:ok

"""
@spec set_log_level(log_level() | integer()) :: :ok
def set_log_level(level) when is_atom(level) do
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this function belongs to reader as it affects all logs of ffmpeg. I suggest we move this to Xav module.

For the c side, it's fine to let it live in reader for now.

case Map.fetch(@log_levels, level) do
{:ok, int} ->
Xav.Reader.NIF.set_log_level(int)

:error ->
raise ArgumentError,
"invalid FFmpeg log level #{inspect(level)}. " <>
"Expected one of #{inspect(Map.keys(@log_levels))} or an integer."
end
end

def set_log_level(level) when is_integer(level) do
Xav.Reader.NIF.set_log_level(level)
end

@doc """
Creates a new reader stream.

Expand Down
2 changes: 2 additions & 0 deletions lib/xav/reader_nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ defmodule Xav.Reader.NIF do
def next_frame(_reader), do: :erlang.nif_error(:undef)

def seek(_reader, _time_in_seconds), do: :erlang.nif_error(:undef)

def set_log_level(_level), do: :erlang.nif_error(:undef)
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ defmodule Xav.MixProject do

def application do
[
extra_applications: [:logger]
extra_applications: [:logger],
mod: {Xav.Application, []}
]
end

Expand Down
20 changes: 20 additions & 0 deletions test/reader_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ defmodule Xav.ReaderTest do
{:error, :eof} = Xav.Reader.next_frame(r)
end

describe "set_log_level/1" do
test "accepts atoms" do
assert :ok = Xav.Reader.set_log_level(:error)
assert :ok = Xav.Reader.set_log_level(:quiet)
# restore default for other tests
assert :ok = Xav.Reader.set_log_level(:info)
end

test "accepts integers" do
assert :ok = Xav.Reader.set_log_level(16)
assert :ok = Xav.Reader.set_log_level(:info)
end

test "rejects unknown atoms" do
assert_raise ArgumentError, ~r/invalid FFmpeg log level/, fn ->
Xav.Reader.set_log_level(:nonsense)
end
end
end

@formats [{"h264", "h264"}, {"h264", "mkv"}, {"vp8", "webm"}, {"vp9", "webm"}, {"av1", "mkv"}]
Enum.map(@formats, fn {codec, container} ->
name = "#{codec} #{container}"
Expand Down