KiteFFmpeg¶
One coroutine-first Kotlin API for video and audio. Decode, encode, transcode and filter media from a single suspend-friendly surface, backed by FFmpeg's libav* libraries. Kotlin/Native uses cinterop; JVM and Android use a narrow JNI bridge; wasmJs uses a generated binding over a wasm module you load. The JVM jar carries a macOS arm64 library only, so JVM consumers on other hosts, along with js, get an invariant unsupported placeholder contract. There is no ffmpeg subprocess, and memory stays constant regardless of input length.
// One call: demux -> decode -> filter -> encode -> mux, in a single pass.
Transcoder.transcode(
input = "input.mp4",
output = "output.mp4",
spec = VideoEncoderSpec(
// mpeg4 is the dependency-free baseline present in every FFmpeg profile.
codec = CodecId("mpeg4"),
width = 320, height = 180,
frameRate = Rational(30, 1),
bitrateBps = 1_500_000,
),
videoFilter = "scale=320:180,hue=b=0.1,vignette,format=yuv420p",
audioSpec = AudioEncoderSpec(codec = CodecId.Aac), // null drops audio
audioFilter = "volume=0.8", // optional
onProgress = { p -> println("encoded ${p.framesEncoded} frames") },
)
For H.264 or H.265, probe first and pick what the linked build has.
h264_videotoolbox has standing macOS runtime evidence; libx264 exists only in a GPL FFmpeg.
The generated Android profile contains MediaCodec names, but the current Android claim stops at
source, host tests, link and packaging, with exact named-decoder selection documented in
Decoding. See Platform support and Licensing.
- Getting started: install FFmpeg, wire the build, run your first transcode.
- API reference: every public type and signature.
Why KiteFFmpeg¶
Media work from Kotlin normally means launching the ffmpeg CLI and parsing its stderr, or wrapping a prebuilt binary like FFmpegKit. You build arguments into a string, launch a process, and read progress back out of log lines. The codec engine lives outside your program.
KiteFFmpeg is a single Kotlin API over libav* directly. You call Transcoder.transcode(...) and it opens the file via libavformat, demuxes once, routes packets to per-stream libavcodec decoders, pushes frames through libavfilter graphs, encodes, and interleaves the streams into a valid container. There is no process to spawn and no log output to parse. Progress arrives as a typed callback, errors arrive as typed exceptions, and frames flow as a coroutine Flow.
Everything routes through one demux pass. When you decode several streams, or composite two inputs, the demuxer reads the file a single time and fans packets out to the decoders that need them.
Install¶
That is the whole setup. FFmpeg is compiled into each published artifact, so there is nothing to install and nothing to configure. The rest of this page is about working INSIDE the KiteFFmpeg checkout, where you build FFmpeg yourself.
JVM and Android are published artifacts, not source-only actuals. The Android AAR on Maven Central
declares minSdkVersion 26 in its own manifest and carries libkitecodec_jni.so for arm64-v8a
and x86_64 with 16 KiB ELF/app packaging. The JVM jar carries a macOS arm64 library and only
that one, so a JVM consumer on Linux or Windows gets the typed unavailable placeholder rather than a
codec. Android and iOS play real media on real phones as the engine under
KitePlayer; what they lack is an automated device job in
this repository's CI. wasmJs is a real playback backend once its wasm module is loaded, and js
is an unsupported placeholder that reports no capabilities and rejects media operations
predictably. See
Platform support.
What you can do¶
Read¶
Open a file, inspect its streams, and pull decoded frames as a Flow. The demuxer wraps AVFormatContext; the decode loop is EAGAIN-correct and promotes best_effort_timestamp to pts.
MediaSource.open("input.mp4").use { src ->
println("${src.formatName}, ${src.durationMicros} us")
val video = src.primaryVideo ?: error("no video stream")
src.decodedFrames(video).collect { frame ->
try {
val info = frame.info // width, height, pts, pixelFormat...
val pixels = frame.copyPlanesToByteArray()
// ...
} finally {
frame.close() // emitted frames are OWNED; close or they leak
}
}
}
Read a single frame for a thumbnail and encode it straight to image bytes:
MediaSource.open("input.mp4").use { src ->
src.extractFrame(atMicros = 90_000_000).use { frame ->
writeFile(frame.encodeImage(CodecId.Mjpeg))
}
}
See Decoding & frames.
Transcode¶
The high-level pipeline handles trim, audio copy, subtitle copy, metadata and progress in one call. Skip the video codec (spec = null) for an audio-only transcode such as mp3 to aac.
// Frame-exact clip, output timestamps rebased to zero.
Transcoder.transcode(
input = "input.mp4",
output = "clip.mp4",
spec = VideoEncoderSpec(
codec = CodecId("mpeg4"),
width = 1280, height = 720,
frameRate = Rational.Fps30,
),
audioCopy = true, // stream-copy audio bit-exact
startMicros = 12_300_000,
endMicros = 45_600_000,
metadata = mapOf("title" to "My clip"),
)
Nothing to re-encode at all? Rewrite the container losslessly:
See Transcoding and Remuxing.
Filter¶
Build a libavfilter graph from a plain FFmpeg filter description and drive frames through it. Single-input graphs expose a Flow pipeline; multi-input graphs (overlay, amix) take a push callback.
// Two inputs -> one output: watermark in the bottom-right corner.
val graph = FilterGraph.buildVideoMulti(
"[in0][in1]overlay=W-w-10:H-h-10[out]",
listOf(mainVideoInput, logoInput),
)
graph.feedInput(0, videoFrame) { composited -> /* encode */ }
graph.feedInput(1, logoFrame) { /* ... */ }
See Filtering.
Guides¶
| Getting started | Install FFmpeg, build, and run your first transcode. |
| Transcoding | The Transcoder.transcode pipeline: specs, trim, audio copy, progress. |
| Decoding & frames | MediaSource, decode flows, single-pass multi-stream, thumbnails. |
| Filtering | FilterGraph video and audio graphs, single and multi-input. |
| Encoding & muxing | MediaSink, VideoEncoder / AudioEncoder, encoder specs and options. |
| Remuxing | Lossless container rewrite and keyframe-snapped trim. |
| Concurrency | Threading, confinement, and cancellation rules. |
| Recipes | Copy-paste patterns for common tasks. |
| Platform support | What runs where, and how FFmpeg is sourced. |
| Licensing | LGPL/GPL flavors and what shipping them obligates. |
| Troubleshooting | FFmpeg discovery, Windows setup, VMs, NDK. |
Status¶
KiteFFmpeg is pre-1.0 and actively developed. The public pipeline is implemented for Kotlin/Native, JVM and Android over the same common contracts, and everything is published on Maven Central with FFmpeg embedded. The JVM JNI boundary is proved by 89 tests over real FFmpeg on an arm64 Mac. Android and iOS device evidence is hand-verified and app-shipped rather than automated in this repository's CI.
One target table covers the whole project and lives in the README. For the design, the FFmpeg sourcing modes, and what is next, see About KiteFFmpeg.