How to Use FFmpeg with Kotlin (No Installation Required)

You need video processing in your Kotlin app. Maybe you're building a Spring Boot backend that transcodes user uploads, an Android app that generates social clips, or a Ktor service that watermarks content before delivery. You search "ffmpeg kotlin" and find scattered examples, most from 2019.
FFmpeg handles the video processing. But running it from Kotlin means picking between ProcessBuilder (which works but requires FFmpeg on every machine), a JVM wrapper library, or a cloud API that skips the install entirely. This post covers all three with working Kotlin code.
Running FFmpeg from Kotlin with ProcessBuilder
Kotlin runs on the JVM, so you get Java's ProcessBuilder for free. Install FFmpeg on the machine, then shell out to it:
val process = ProcessBuilder(
"ffmpeg",
"-i", "input.mp4",
"-c:v", "libx264",
"-crf", "23",
"-preset", "medium",
"-c:a", "aac",
"-b:a", "128k",
"output.mp4"
).redirectErrorStream(true).start()
process.inputStream.bufferedReader().forEachLine { println(it) }
val exitCode = process.waitFor()
check(exitCode == 0) { "FFmpeg failed with exit code $exitCode" }
This is the simplest path. But you own the FFmpeg install on every server, every CI runner, every Docker image. On Docker, FFmpeg adds 80-200MB. On serverless (Lambda, Cloud Functions), you can't install system binaries at all and the 250MB package limit is tight.
Using Jaffree for a Fluent API
Jaffree is the best JVM wrapper for FFmpeg. It works perfectly from Kotlin and gives you a builder API instead of string arrays:
// build.gradle.kts
dependencies {
implementation("com.github.kokorin.jaffree:jaffree:2023.09.10")
}
import com.github.kokorin.jaffree.ffmpeg.FFmpeg
import com.github.kokorin.jaffree.ffmpeg.UrlInput
import com.github.kokorin.jaffree.ffmpeg.UrlOutput
FFmpeg.atPath()
.addInput(UrlInput.fromUrl("input.mp4"))
.addOutput(
UrlOutput.toUrl("output.mp4")
.addArguments("-c:v", "libx264")
.addArguments("-crf", "23")
.addArguments("-preset", "medium")
.addArguments("-c:a", "aac")
.addArguments("-b:a", "128k")
)
.execute()
Jaffree also supports progress tracking, which is useful for showing users a progress bar:
FFmpeg.atPath()
.addInput(UrlInput.fromUrl("input.mp4"))
.addOutput(
UrlOutput.toUrl("output.mp4")
.addArguments("-c:v", "libx264")
.addArguments("-crf", "23")
)
.setProgressListener { progress ->
println("Frame: ${progress.frame}, Time: ${progress.timeMark}")
}
.execute()
Better than raw ProcessBuilder. But Jaffree still needs FFmpeg on the host. It's a wrapper, not a replacement. You still manage the binary across environments.
Processing Video via Cloud API (No FFmpeg Install)
If you don't want FFmpeg on your machines, offload the work to a cloud API. FFmpeg Micro gives you full FFmpeg capabilities through HTTP. Here's a minimal example using OkHttp, the most common HTTP client in the Kotlin ecosystem:
// build.gradle.kts
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.json:json:20240303")
}
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import org.json.JSONArray
val client = OkHttpClient()
val apiKey = System.getenv("FFMPEG_MICRO_API_KEY")
val body = JSONObject().apply {
put("inputs", JSONArray().put(JSONObject().put("url", "https://example.com/input.mp4")))
put("outputFormat", "mp4")
put("preset", JSONObject().apply {
put("quality", "high")
put("resolution", "1080p")
})
}.toString()
val request = Request.Builder()
.url("https://api.ffmpeg-micro.com/v1/transcodes")
.header("Authorization", "Bearer $apiKey")
.header("Content-Type", "application/json")
.post(body.toRequestBody("application/json".toMediaType()))
.build()
val response = client.newCall(request).execute()
val result = JSONObject(response.body!!.string())
val jobId = result.getString("id")
println("Job created: $jobId")
No binary to install. No Docker image bloat. No codec management. Send a video URL, pick your settings, get results back.
For advanced operations, pass raw FFmpeg options instead of presets:
val advancedBody = JSONObject().apply {
put("inputs", JSONArray().put(JSONObject().put("url", "https://example.com/input.mp4")))
put("outputFormat", "webm")
put("options", JSONArray().apply {
put(JSONObject().put("option", "-c:v").put("argument", "libvpx-vp9"))
put(JSONObject().put("option", "-crf").put("argument", "30"))
put(JSONObject().put("option", "-b:v").put("argument", "0"))
})
}.toString()
Polling for Completion
Transcode jobs are async. Poll until the job finishes:
var status = "queued"
while (status == "queued" || status == "processing") {
Thread.sleep(2000)
val statusRequest = Request.Builder()
.url("https://api.ffmpeg-micro.com/v1/transcodes/$jobId")
.header("Authorization", "Bearer $apiKey")
.get()
.build()
val statusResponse = client.newCall(statusRequest).execute()
status = JSONObject(statusResponse.body!!.string()).getString("status")
println("Status: $status")
}
Once the status is completed, grab the download URL:
val downloadRequest = Request.Builder()
.url("https://api.ffmpeg-micro.com/v1/transcodes/$jobId/download")
.header("Authorization", "Bearer $apiKey")
.get()
.build()
val downloadResponse = client.newCall(downloadRequest).execute()
val downloadUrl = JSONObject(downloadResponse.body!!.string()).getString("url")
// Signed URL valid for 10 minutes
Using Ktor Client Instead
If you're already using Ktor, use its built-in HTTP client instead of adding OkHttp:
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("io.ktor:ktor-client-cio:2.3.12")
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
}
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
val client = HttpClient(CIO)
val response = client.post("https://api.ffmpeg-micro.com/v1/transcodes") {
header("Authorization", "Bearer $apiKey")
contentType(ContentType.Application.Json)
setBody("""
{
"inputs": [{"url": "https://example.com/input.mp4"}],
"outputFormat": "mp4",
"preset": {"quality": "high", "resolution": "1080p"}
}
""".trimIndent())
}
println(response.bodyAsText())
Ktor's client is lighter than OkHttp and plays well with coroutines if your backend is already async.
CLI vs. API: Side-by-Side
Same operation (converting MP4 to 720p WebM) done both ways:
FFmpeg CLI (requires local install):
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 -vf scale=-2:720 output.webm
FFmpeg Micro API (no install):
val body = JSONObject().apply {
put("inputs", JSONArray().put(JSONObject().put("url", "https://example.com/input.mp4")))
put("outputFormat", "webm")
put("options", JSONArray().apply {
put(JSONObject().put("option", "-c:v").put("argument", "libvpx-vp9"))
put(JSONObject().put("option", "-crf").put("argument", "30"))
put(JSONObject().put("option", "-b:v").put("argument", "0"))
put(JSONObject().put("option", "-vf").put("argument", "scale=-2:720"))
})
}.toString()
The CLI version needs FFmpeg installed. The API version runs from any Kotlin app with an HTTP client.
Common Pitfalls
ProcessBuilder path issues on macOS. Kotlin scripts and Gradle tasks don't always inherit your shell PATH. If FFmpeg isn't found, pass the full path (/usr/local/bin/ffmpeg or /opt/homebrew/bin/ffmpeg on Apple Silicon).
Blocking the main thread in Android. Never call ProcessBuilder or make HTTP requests on the main thread. Wrap everything in a coroutine with Dispatchers.IO:
withContext(Dispatchers.IO) {
val response = client.newCall(request).execute()
// handle response
}
Jaffree version compatibility. Some older Jaffree versions have issues with FFmpeg 6.x. Use 2023.09.10 or newer.
Large file uploads. For files over 100MB, use FFmpeg Micro's presigned upload flow instead of passing a public URL. Generate a presigned URL, PUT the file directly to cloud storage, confirm the upload, then reference the GCS URL in your transcode request.
FAQ
Can I use FFmpeg on Android directly? Not easily. Android doesn't include FFmpeg, and cross-compiling it for ARM is painful. Mobile FFmpeg (now FFmpeg Kit) works but adds 20-50MB to your APK. A cloud API avoids all of this.
Does Kotlin Multiplatform work with FFmpeg? On JVM targets, yes (ProcessBuilder or Jaffree). On Native or JS targets, no. A cloud API works from any Kotlin target since it's just HTTP.
What's the cost of using a cloud API vs self-hosting? FFmpeg Micro has a free tier for testing and small workloads. For production, pricing is per-minute of video processed. Self-hosting costs server time, maintenance, and your attention when codecs break after an OS update.
Is the API fast enough for real-time processing? FFmpeg Micro is designed for batch and async workflows, not real-time streaming. Jobs typically complete in seconds for short videos. For live streaming, you need a different tool.
How do I handle errors from the API? Check the HTTP status code. 400 means bad request (wrong parameters). 402 means you've hit your quota. 401 means your API key is invalid or expired. The response body always includes an error message.
*Last verified: June 2026. Code examples tested against FFmpeg 7.x and FFmpeg Micro API v1.*
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

How to Use FFmpeg with Java (No Installation Required)
Learn three ways to process video in Java: ProcessBuilder, Jaffree, and a cloud API that needs no FFmpeg install. Working code for each approach.

How to Use FFmpeg with TypeScript (No Installation Required)
Learn how to process video in TypeScript without installing FFmpeg. Compare child_process, fluent-ffmpeg, ffmpeg.wasm, and cloud API approaches with typed code.

How to Use FFmpeg with Go (Golang)
Learn 3 ways to use FFmpeg with Go: os/exec, ffmpeg-go wrapper, and a cloud API. Working code for each approach, common pitfalls, and when to use which.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free