Skip to content

Troubleshooting

An app that uses the published artifacts needs nothing but the dependency line, because FFmpeg is inside them. Most problems on this page come from building KiteFFmpeg itself from this repository, where the build must find or produce an FFmpeg tree; Building from source explains where it looks.

"No FFmpeg install found for \<target>"

FFmpegPaths.resolve looks for a static tree under native-libs/<license>/<target>/{include,lib} first, then falls back to a system install. This error means that neither exists. Fix it in one of these ways:

  • Build a static tree with ./gradlew :kiteffmpeg:buildFFmpegFor<Target> (see the prerequisites below), or download the release tree as Building from source shows.
  • Install FFmpeg with Homebrew (brew install ffmpeg). A system FFmpeg works only for the host's own desktop target, never for a cross target. On Linux, use the release tree instead, because a Kotlin/Native link against the apt packages fails.

Note the <license> path segment: if you put your own GPL tree under native-libs/gpl/<target>/ but did not pass -Pkiteffmpeg.ffmpeg.license=gpl, the build looks under native-libs/lgpl/ and misses your libraries. The folder and the property must match. KiteFFmpeg itself builds only LGPL trees.

"requires its complete vendored ... FFmpeg tree"

-Pkiteffmpeg.phoneTargetsOnly=true checks the tree of each Apple and Android target before it builds. Each tree under native-libs/lgpl/<target>/ must hold include/libavformat/avformat.h, the six libraries libavcodec.a, libavformat.a, libavutil.a, libavfilter.a, libswscale.a and libswresample.a, and the configure record lib/kiteffmpeg/ffmpeg-configure.txt. Run the buildFFmpegFor<Target> task that the message names. Nothing is downloaded to fill a gap.

A bake that finishes without one of those libraries stops with "FFmpeg reported a successful install but ... is missing ...". Check the configure and make output that the task printed.

An app that uses the published artifacts never sees either message, because FFmpeg is compiled in.

macOS: Homebrew in a non-standard prefix

On macOS the build probes /opt/homebrew (Apple Silicon) and /usr/local (Intel) for include/libavformat/avformat.h. If your Homebrew is elsewhere, or you want to point at a custom FFmpeg prefix, set the override in gradle.properties:

kiteffmpeg.macos.homebrew.prefix=/custom/prefix

The prefix must contain include/libavformat/avformat.h and the FFmpeg dylibs under lib/.

Windows: nothing is auto-discovered

There is no system-FFmpeg discovery on Windows. FFmpegPaths resolves Homebrew (macOS) and apt (Linux) installs only. For mingwX64 it requires a populated native-libs/<license>/mingw-x64/ tree. Installing an ffmpeg.exe from anywhere will not help. The build needs headers and import libraries.

Stage them yourself, either by unzipping this repository's own ffmpeg-<version>-lgpl-mingw-x64.zip release asset under native-libs/lgpl/mingw-x64 (this is what CI does) or by cross-compiling the vendored build with a mingw-w64 toolchain. The steps are in Building from source. The release tree is static, so no DLL has to be on PATH at run time.

VideoToolbox fails on VMs / CI runners

h264_videotoolbox refuses to open when no hardware encode block is available. That is the usual case on virtualized macOS (CI runners, VMs). The encoder returns an error at addVideoEncoder / during transcode even though FFmpeg.hasEncoder("h264_videotoolbox") is true, because availability of the encoder and availability of the hardware are different questions.

Pass allow_sw to let VideoToolbox fall back to its software path instead of failing:

VideoEncoderSpec(
    codec = CodecId.H264,
    encoder = EncoderId.H264VideoToolbox,
    width = 1280, height = 720,
    frameRate = Rational(30, 1),
    options = mapOf("allow_sw" to "1"),
)

Vendored build prerequisites

The buildFFmpegFor<Target> tasks compile FFmpeg from source. They fail early when something they need is missing.

1. The source checkouts. The task expects git checkouts of FFmpeg at vendor/ffmpeg and of dav1d at vendor/dav1d, and stops with the clone command for whichever is missing:

git clone --depth 1 --branch n9.0.2 https://github.com/FFmpeg/FFmpeg.git vendor/ffmpeg
git clone --depth 1 --branch 1.5.4 https://code.videolan.org/videolan/dav1d.git vendor/dav1d

Each checkout must hold exactly the commit its tag names upstream, which the build task pins beside the tag, with nothing changed, added or ignored in it. The output carries the tag's name and records only the patches the build applied, so a build from anything else would name a release it is not. A checkout at another commit, one with a change in it, and a directory that is part of another repository rather than a checkout of its own all stop the build before it compiles anything, and the message lists what git reports. Move a change to FFmpeg into a patch under native/patches/ffmpeg, and set the checkout's changes aside with git -C vendor/ffmpeg stash push --all. A change to libass goes under native/patches/libass the same way, and dav1d and the rest of the libass chain carry no patches.

2. Build tools. make, a C toolchain (clang/gcc) and nasm (x86 assembly: configure fails without it on x86 targets). dav1d's build, which every FFmpeg build runs first, also needs meson and ninja:

brew install nasm meson ninja

3. That is the whole list. No third-party encoder or text library is linked on any target, so configure never asks the host package manager for one. Apple targets use SDK zlib plus VideoToolbox/AudioToolbox from the SDK; Linux and Windows use konan's own sysroot; Android uses the NDK. There are no GPL tasks.

Path safety. A checkout or final output path may contain #. Configure, make and install run only in a unique hash-free workspace under java.io.tmpdir; source copying excludes .git and every build subtree. After install, the normalized configure invocation is written as exactly one line at lib/kiteffmpeg/ffmpeg-configure.txt; verification and packaging require that record, and packaging does not consult a vendor build log. On success, the verified install is copied to a sibling staging directory and replaces the output. On failure, the old output remains and the retained scratch path is printed for diagnosis.

4. When a build runs again. Gradle runs a bake again when its output tree changed or one of its inputs differs from the last successful run: the build logic, the target, the license, the pinned tag and commit, the commit the checkout holds and any change in it, the patches, or the self-contained switch. Otherwise it reports UP-TO-DATE. A machine with no record of an earlier run, such as a fresh CI runner with a restored tree, builds again. To force a rebuild, pass --rerun to the task or delete its output directory.

Android: "Android NDK not found"

The NDK cross-compile resolves its toolchain from, in order: the ANDROID_NDK_HOME, ANDROID_NDK_ROOT, or ANDROID_NDK_LATEST_HOME environment variables, then the newest ndk/<version> under the default SDK locations (~/Library/Android/sdk/ndk on macOS, ~/Android/Sdk/ndk on Linux). If none resolve:

export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk/<version>
./gradlew :kiteffmpeg:buildFFmpegForAndroidArm64

The vendored vendor/ffmpeg clone is required here too. The repository's Android FFmpeg builds always use the LGPL MediaCodec profile. There is no GPL Android profile, and requesting one fails with an explanatory error.

Android: regular AAR/JNI versus androidNative*

These are separate target models. buildFFmpegForAndroid* plus compileKotlinAndroidNative* produces Kotlin/Native klibs. The regular Android KMP source model uses a dynamically registered JNI bridge, is minSdk 26, and packages only arm64-v8a, armeabi-v7a and x86_64 inputs, with 16 KiB ELF/app-packaging checks on the two 64-bit ABIs. To build it locally on its own, pass -Pkiteffmpeg.phoneTargetsOnly=true, which needs ANDROID_SDK_ROOT, ANDROID_NDK_HOME and the complete local FFmpeg trees.

The Android AAR is on Maven Central. It declares minSdkVersion 26 and carries the three ABIs above. FFmpeg's MediaCodec wrapper is selected only by an FFmpeg codec name after the Android loader accepts the linked FFmpeg identity and attaches the VM; KiteFFmpeg does not call the platform codec API directly.

"libx264 not found" / EncoderId.Libx264 encoder missing at runtime

libx264 exists only in a GPL FFmpeg. A system FFmpeg from Homebrew or apt usually has it, but no KiteFFmpeg artifact does, and no KiteFFmpeg task builds one.

Two ways forward:

  • Use an encoder that is actually there. FFmpeg.encodersFor(CodecId.H264) lists them: EncoderId.H264VideoToolbox or EncoderId.HevcVideoToolbox on macOS, h264_mediacodec on Android, or the universal mpeg4 baseline, which every build carries. No Android hardware encoder has run on a device yet.
  • Link an FFmpeg tree you built. Put it under native-libs/gpl/<target>/ and select it with -Pkiteffmpeg.ffmpeg.license=gpl. Read the license consequences first: it makes your whole application GPL.

Either way, probe at runtime with FFmpeg.hasEncoder("libx264") before committing to a codec.

Why FFmpeg refused a file

FFmpeg returns a number when it refuses something and says why only in its log, so the code alone reads "Invalid data found when processing input" for a cut MP4, a broken playlist and a file that is not media at all. A failed open keeps what FFmpeg logged for it, with no log sink needed:

try {
    MediaSource.open(path)
} catch (failure: FFmpegException) {
    failure.error          // the type, from FFmpeg's code, as always
    failure.logged         // [mov,mp4,m4a,3gp,3g2,mj2] moov atom not found
}

logged holds the lines FFmpeg logged at error level or worse, on the calling thread, while the call ran, at most eight, and the exception's message ends with them too. Opening a source, a decoder, a subtitle decoder or converter, a sink, an encoder or a copied stream, writing the header and building a filter graph keep them. Reads, decodes and writes do not; install a sink with FFmpeg.setLogSink to hear those.

Still stuck?

Check the capability probe first. It tells you which FFmpeg you actually linked:

println(FFmpeg.versions)
println(FFmpeg.buildConfiguration)   // the exact ./configure line of the linked FFmpeg

Then open an issue with the probe output, your platform, and how you sourced FFmpeg.