Skip to content

Choosing implementations

Mattias Carlsson edited this page Sep 3, 2026 · 1 revision

Choosing implementations: mutex groups and the precedence ladder

FFmpeg can be built against more than one library for the same job — two H.264 encoders, four TLS backends, three AV1 encoders. mediaforge does not build all of them. Each such job is a mutex group: exactly one implementation wins, and the recipes for the others are disabled for that build.

For the flag syntax and the accepted values, see Documentation/usage.md. This page is about how a choice is reached when you did not spell it out, and why the defaults are what they are.

The six groups

There are six: TLS, AAC, H.264, H.265, AV1 encoder, and SPIR-V. Their flags, accepted values and defaults are listed in Documentation/usage.md and are not repeated here — that file is checked by the every-flag-is-documented gate on every push, and a second copy on a wiki nothing checks would be free to drift from it.

In the source, the built-in defaults live in one place as *_DEFAULT_BUILTIN variables (lib/resolve.sh:21-26). ./mediaforge.sh --list-pkgs prints every recipe with its category and mutex group, which is the authoritative answer to "what is actually in this group".

Two flags look like group selectors and are not:

  • --flite-audio= (none|alsa|pulseaudio|oss|sun, default none) picks a backend inside one recipe rather than between recipes. The FFmpeg filter never invokes flite's audio output, so none drops au_alsa.o from libflite.a and keeps -lasound out of a static link.
  • --openssldir= is a path, not a choice. See the trust store section below.

The precedence ladder

Six rungs. Each one only fills a choice still unset by the rung above it.

# Source Where
1 CLI flag--tls=openssl option parser
2 --menu selections run_menu, before the resolver
3 Stored choices from the last successful build load_stored_choices, mediaforge.sh:409
4 Interactive prompt ("Pick a TLS backend") resolve_choices, mediaforge.sh:415
5 Profile default*_DEFAULT from profiles/*.conf lib/resolve.sh:250-255
6 Built-in default*_DEFAULT_BUILTIN lib/resolve.sh:21-26

The rung that surprises people is 3 above 4. load_stored_choices runs before resolve_choices and assigns with :=, so a stored value leaves the variable non-empty and the prompt's [ -z "$TLS_BACKEND" ] test never fires. Once you have built successfully, you stop being asked. That is deliberate — a second build of the same tree should not re-interrogate you — but it means a choice you made once silently governs every later build until you change it.

To get the prompts back:

./mediaforge.sh build --clean-choices    # delete $PREFIX/.mediaforge-choices

--menu (rung 2) bypasses the store entirely: load_stored_choices returns early when USE_MENU=true, so the menu always starts from the defaults rather than from what you picked last time.

Where choices are stored

$PREFIX/.mediaforge-choices, written by save_stored_choices after a successful resolve:

# Generated by mediaforge — edit at your own risk; --clean-choices removes this file.
STORED_TLS_BACKEND=gnutls
STORED_AAC_IMPL=native
STORED_H264_IMPL=x264
STORED_H265_IMPL=x265
STORED_AV1_ENC_IMPL=svtav1
STORED_SPIRV_IMPL=glslang
STORED_OPENSSLDIR=''

--dry-run skips both reading and writing it, so a dry run can never change what your next real build does.

The file is parsed by name, not sourced. It used to be sourced, which made a stored string and arbitrary code the same thing on the next build; _stored_choice replaced that with a by-name parser. This is worth knowing if you edit the file by hand: only the seven STORED_* keys above are read, and the value is taken as the remainder of the line after the first =.

What each choice is, in the project's own words

These are the descriptions the interactive prompts show (lib/resolve.sh:196-236), which are the authoritative summary of why one option is the default and what you give up by moving off it.

Group Options
TLS gnutls GnuTLS — free, default · openssl OpenSSL — Apache 2.0 · mbedtls mbedTLS — small footprint · libressl LibreSSL libtls · none no TLS support
AAC native FFmpeg native AAC (always available) · fdk_aac Fraunhofer FDK-AAC (requires --enable-nonfree)
H.264 x264 GPL, de-facto standard · openh264 BSD source, MPEG-LA royalties apply
H.265 x265 GPL · kvazaar LGPL
AV1 svtav1 fastest, recommended · rav1e pure Rust · av1 libaom reference encoder, slow
SPIR-V glslang Khronos reference, simpler build · shaderc Google wrapper, heavier build

Two consequences worth pulling out. Choosing x264 or x265 means the build needs --enable-gpl; choosing fdk_aac means it needs --enable-nonfree and produces a binary that cannot be redistributed. And rav1e is the reason cargo appears in the optional requirements — without it that recipe is skipped with a warning rather than failing the build.

The one exception to the ladder

--enable-nonfree implies --aac=fdk_aac unless you picked an AAC encoder explicitly this run (lib/resolve.sh:245).

This deliberately jumps the ladder. A stored native from a previous free build would otherwise silently outrank the --enable-nonfree you just typed, and you would get a nonfree build that had not enabled the one encoder nonfree is usually for. An explicit --aac=native on the same command line still wins — the check is against the CLI value captured before the store was consulted, not against the resolved one.

So the full rule for AAC is: explicit --aac= this run > --enable-nonfree implication > the ordinary six-rung ladder.

The trust store, and why --openssldir is fussy

--openssldir=PATH sets the trust store compiled into the openssl and libressl arms. It must be absolute — the value is baked into the library, so a relative path would resolve against whatever the linking process's working directory happened to be.

Unset, resolve_openssldir() probes a candidate list for a directory holding cert.pem, and falls back to $PREFIX/etc/ssl:

/etc/ssl  /etc/pki/tls  /usr/local/etc/ssl
/opt/homebrew/etc/ca-certificates  /usr/local/etc/ca-certificates

This matters most for --tls=libressl, because libtls has no runtime override at all — no environment variable reads the trust store path, so the baked-in value is the only default a user gets. recipes/crypto/libressl.sh stages LibreSSL's bundled cert.pem on the fallback path and lib/install.sh ships it.

The value is validated hard (_validate_openssldir, lib/resolve.sh): absolute, no .. segments, and a conservative character allowlist rather than a blocklist of shell metacharacters. The reason is that this path is both stored in the choices file and used as an install destination that may be written with sudo.

LTO is off by default

--enable-lto / --disable-lto opts SVT-AV1 — and any future LTO-aware recipe — into link-time optimization. It is off by default, and that is not caution for its own sake: GCC's LTO IR is not forward-compatible across major versions, so an LTO-baked .a stops linking after a distribution GCC bump. On a rolling release that is a quarterly breakage of every downstream consumer of the prefix.

--debug forces LTO off at every level regardless, because LTO objects hold GIMPLE rather than DWARF — an LTO'd debug build has no debug info to speak of.

See also

  • Documentation/usage.md — the full flag reference
  • Worked examples — these choices in context
  • ./mediaforge.sh build --menu — pick interactively instead
  • ./mediaforge.sh --list-pkgs — every recipe with its category and group