Skip to content

Commit bd90134

Browse files
authored
Add set_log_level on ffmpeg (#46)
* Add set_log_level on ffmpeg * Move set_log_level to Xav module; rename config key to :ffmpeg_log_level
1 parent 5664758 commit bd90134

7 files changed

Lines changed: 193 additions & 4 deletions

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,33 @@ def deps do
2424
end
2525
```
2626

27+
## Controlling FFmpeg log output
28+
29+
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
30+
31+
```
32+
[swscaler @ 0x1490a0000] No accelerated colorspace conversion found from yuv420p to rgb24.
33+
```
34+
35+
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.
36+
37+
You can raise the threshold from Elixir:
38+
39+
```elixir
40+
Xav.set_log_level(:error)
41+
```
42+
43+
Or set it once at application start by configuring your application env:
44+
45+
```elixir
46+
# config/runtime.exs
47+
config :xav, ffmpeg_log_level: :error
48+
```
49+
50+
`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.
51+
52+
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.
53+
2754
## Usage
2855

2956
Decode

c_src/xav/xav_reader.c

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,31 @@ void free_xav_reader(ErlNifEnv *env, void *obj) {
327327
}
328328
}
329329

330-
static ErlNifFunc xav_funcs[] = {{"new", 9, new},
331-
{"next_frame", 1, next_frame, ERL_NIF_DIRTY_JOB_CPU_BOUND},
332-
{"seek", 2, seek, ERL_NIF_DIRTY_JOB_CPU_BOUND}};
330+
/* Wraps av_log_set_level(int). The level integer is validated on the
331+
* Elixir side (Xav.Reader.set_log_level/1 maps atoms to the AV_LOG_*
332+
* constants) so the NIF only needs to pass it through. Note that
333+
* av_log_set_level() is process-global: it changes the log level for
334+
* every libav* call made from the current OS process, not just the
335+
* reader that triggered the call. */
336+
ERL_NIF_TERM set_log_level(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
337+
if (argc != 1) {
338+
return xav_nif_raise(env, "invalid_arg_count");
339+
}
340+
341+
int level;
342+
if (!enif_get_int(env, argv[0], &level)) {
343+
return xav_nif_raise(env, "invalid_log_level");
344+
}
345+
346+
av_log_set_level(level);
347+
return enif_make_atom(env, "ok");
348+
}
349+
350+
static ErlNifFunc xav_funcs[] = {
351+
{"new", 9, new},
352+
{"next_frame", 1, next_frame, ERL_NIF_DIRTY_JOB_CPU_BOUND},
353+
{"seek", 2, seek, ERL_NIF_DIRTY_JOB_CPU_BOUND},
354+
{"set_log_level", 1, set_log_level}};
333355

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

lib/xav.ex

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,101 @@ defmodule Xav do
1717
media_type: atom()
1818
}
1919

20+
@typedoc """
21+
A human-readable FFmpeg log level.
22+
23+
The mapping to FFmpeg's `AV_LOG_*` constants is:
24+
25+
| Atom | FFmpeg constant | Value |
26+
|-------------|-------------------|-------|
27+
| `:quiet` | `AV_LOG_QUIET` | -8 |
28+
| `:panic` | `AV_LOG_PANIC` | 0 |
29+
| `:fatal` | `AV_LOG_FATAL` | 8 |
30+
| `:error` | `AV_LOG_ERROR` | 16 |
31+
| `:warning` | `AV_LOG_WARNING` | 24 |
32+
| `:info` | `AV_LOG_INFO` | 32 |
33+
| `:verbose` | `AV_LOG_VERBOSE` | 40 |
34+
| `:debug` | `AV_LOG_DEBUG` | 48 |
35+
| `:trace` | `AV_LOG_TRACE` | 56 |
36+
37+
FFmpeg's default is `:info`.
38+
"""
39+
@type log_level ::
40+
:quiet
41+
| :panic
42+
| :fatal
43+
| :error
44+
| :warning
45+
| :info
46+
| :verbose
47+
| :debug
48+
| :trace
49+
50+
@log_levels %{
51+
quiet: -8,
52+
panic: 0,
53+
fatal: 8,
54+
error: 16,
55+
warning: 24,
56+
info: 32,
57+
verbose: 40,
58+
debug: 48,
59+
trace: 56
60+
}
61+
62+
@doc """
63+
Sets the FFmpeg log level.
64+
65+
Accepts either one of the level atoms listed in `t:log_level/0`
66+
or a raw integer level. Returns `:ok`.
67+
68+
This call wraps FFmpeg's `av_log_set_level/1`, which is
69+
**process-global**: the level is shared across every `libav*` and
70+
`libswscale` call in the current OS process, including readers,
71+
decoders, encoders, and converters created from the Elixir VM.
72+
73+
Typical use is to silence the informational `[swscaler @ ...]` lines
74+
that `libswscale` prints when it falls back to a non-SIMD
75+
colorspace conversion path (which happens for example on
76+
`yuv420p -> rgb24` on Apple Silicon):
77+
78+
Xav.set_log_level(:error)
79+
80+
To configure the level declaratively at application start, use the
81+
`:ffmpeg_log_level` key in your application env instead:
82+
83+
# config/runtime.exs
84+
config :xav, ffmpeg_log_level: :error
85+
86+
`Xav.Application` reads this on boot and calls `set_log_level/1`
87+
for you.
88+
89+
## Examples
90+
91+
iex> Xav.set_log_level(:error)
92+
:ok
93+
94+
iex> Xav.set_log_level(:quiet)
95+
:ok
96+
97+
"""
98+
@spec set_log_level(log_level() | integer()) :: :ok
99+
def set_log_level(level) when is_atom(level) do
100+
case Map.fetch(@log_levels, level) do
101+
{:ok, int} ->
102+
Xav.Reader.NIF.set_log_level(int)
103+
104+
:error ->
105+
raise ArgumentError,
106+
"invalid FFmpeg log level #{inspect(level)}. " <>
107+
"Expected one of #{inspect(Map.keys(@log_levels))} or an integer."
108+
end
109+
end
110+
111+
def set_log_level(level) when is_integer(level) do
112+
Xav.Reader.NIF.set_log_level(level)
113+
end
114+
20115
@doc """
21116
Get all available pixel formats.
22117

lib/xav/application.ex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule Xav.Application do
2+
@moduledoc false
3+
4+
use Application
5+
6+
@impl true
7+
def start(_type, _args) do
8+
maybe_set_log_level()
9+
10+
Supervisor.start_link([], strategy: :one_for_one, name: Xav.Supervisor)
11+
end
12+
13+
defp maybe_set_log_level do
14+
case Application.get_env(:xav, :ffmpeg_log_level) do
15+
nil -> :ok
16+
level -> Xav.set_log_level(level)
17+
end
18+
end
19+
end

lib/xav/reader_nif.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ defmodule Xav.Reader.NIF do
1515
def next_frame(_reader), do: :erlang.nif_error(:undef)
1616

1717
def seek(_reader, _time_in_seconds), do: :erlang.nif_error(:undef)
18+
19+
def set_log_level(_level), do: :erlang.nif_error(:undef)
1820
end

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ defmodule Xav.MixProject do
4040

4141
def application do
4242
[
43-
extra_applications: [:logger]
43+
extra_applications: [:logger],
44+
mod: {Xav.Application, []}
4445
]
4546
end
4647

test/xav_test.exs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule XavTest do
2+
use ExUnit.Case, async: false
3+
4+
describe "set_log_level/1" do
5+
test "accepts atoms" do
6+
assert :ok = Xav.set_log_level(:error)
7+
assert :ok = Xav.set_log_level(:quiet)
8+
# restore default for other tests
9+
assert :ok = Xav.set_log_level(:info)
10+
end
11+
12+
test "accepts integers" do
13+
assert :ok = Xav.set_log_level(16)
14+
assert :ok = Xav.set_log_level(:info)
15+
end
16+
17+
test "rejects unknown atoms" do
18+
assert_raise ArgumentError, ~r/invalid FFmpeg log level/, fn ->
19+
Xav.set_log_level(:nonsense)
20+
end
21+
end
22+
end
23+
end

0 commit comments

Comments
 (0)