MP4-to-MP3 is a pure audio-extraction job — FFmpeg handles it in a single pass with no re-encoding of the video stream. Kotlin can call FFmpeg directly via ProcessBuilder, or use the ffmpeg-kt wrapper for a more idiomatic API. If you're building a JVM service and don't want to manage an FFmpeg install, the ChangeThisFile API offloads it entirely.

Method 1: ProcessBuilder + FFmpeg (direct, no extra deps)

FFmpeg is the industry standard for audio/video processing. If FFmpeg is installed on the system, ProcessBuilder gives you full control with zero extra JVM dependencies.

# Install FFmpeg
apt install ffmpeg          # Debian/Ubuntu
brew install ffmpeg          # macOS
choco install ffmpeg         # Windows
import java.io.File

fun mp4ToMp3(inputPath: String, outputPath: String, bitrate: String = "192k"): Int {
    val process = ProcessBuilder(
        "ffmpeg",
        "-i", inputPath,
        "-vn",              // drop video stream
        "-acodec", "libmp3lame",
        "-ab", bitrate,
        "-map", "a",
        "-y",               // overwrite output
        outputPath
    )
        .redirectErrorStream(true)
        .start()

    // Capture output for logging / error diagnosis
    val output = process.inputStream.bufferedReader().readText()
    val exitCode = process.waitFor()

    if (exitCode != 0) {
        System.err.println("FFmpeg failed (exit $exitCode):\n$output")
    }
    return exitCode
}

fun main() {
    val result = mp4ToMp3("video.mp4", "audio.mp3", bitrate = "192k")
    if (result == 0) println("Done: audio.mp3")
}

Bitrate guide:

  • 128k — Streaming quality. Acceptable for speech, podcasts.
  • 192k — Good default. Transparent for most music.
  • 256k / 320k — Near-lossless. Larger files; diminishing returns.

-vn drops the video stream entirely. -map a selects all audio tracks if the MP4 has multiple. -y prevents FFmpeg from hanging on an interactive prompt when the output file exists.

Method 2: ffmpeg-kt wrapper (Kotlin-idiomatic DSL)

ffmpeg-kt wraps FFmpeg's ProcessBuilder invocation in a Kotlin coroutine-friendly DSL, with structured progress callbacks and suspend-friendly execution.

// build.gradle.kts
dependencies {
    implementation("io.github.oleksandrbalan:ffmpegkt:1.1.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
import io.github.oleksandrbalan.ffmpegkt.FFmpegKit
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val result = FFmpegKit.executeAsync(
        "-i video.mp4 -vn -acodec libmp3lame -ab 192k -map a -y audio.mp3"
    ) { statistics ->
        // Optional: log progress
        print("\rTime: ${statistics.time}ms")
    }

    if (result.returnCode == 0) {
        println("\nDone: audio.mp3")
    } else {
        System.err.println("Failed: ${result.logsAsString}")
    }
}

ffmpeg-kt still requires FFmpeg installed on the system — it's a Kotlin API wrapper, not a bundled binary. The advantage is the progress callback and coroutine-native execution. For Android specifically, use ffmpeg-kit-android which bundles the FFmpeg binary in the AAR.

Method 3: ChangeThisFile API via Ktor HttpClient (no FFmpeg install)

No FFmpeg on the host required. POST the MP4 to /v1/convert and receive MP3. Free tier: 1,000 conversions/month.

# curl reference
curl -X POST https://changethisfile.com/v1/convert \
  -H "Authorization: Bearer ctf_sk_your_key_here" \
  -F "file=@video.mp4" \
  -F "target=mp3" \
  --output audio.mp3
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.io.File

const val API_KEY = "ctf_sk_your_key_here"

suspend fun mp4ToMp3Api(inputPath: String, outputPath: String) {
    val client = HttpClient(CIO) {
        engine {
            requestTimeout = 300_000  // 5 min for large video files
        }
    }
    val response: HttpResponse = client.submitFormWithBinaryData(
        url = "https://changethisfile.com/v1/convert",
        formData = formData {
            append("file", File(inputPath).readBytes(), Headers.build {
                append(HttpHeaders.ContentDisposition, "filename=\"${File(inputPath).name}\"")
                append(HttpHeaders.ContentType, ContentType.Video.MP4)
            })
            append("target", "mp3")
        }
    ) {
        header("Authorization", "Bearer $API_KEY")
    }
    File(outputPath).writeBytes(response.readBytes())
    client.close()
    println("Saved: $outputPath")
}

fun main() = kotlinx.coroutines.runBlocking {
    mp4ToMp3Api("video.mp4", "audio.mp3")
}

The API uses FFmpeg server-side at 192k MP3 by default. Source format is auto-detected — no need to pass source=mp4. Extend requestTimeout for videos longer than a few minutes.

When to use each

ApproachBest forTradeoff
ProcessBuilder + FFmpegSelf-hosted pipelines, full codec controlFFmpeg must be installed; process management complexity
ffmpeg-ktKotlin services needing progress callbacksStill requires FFmpeg installed; adds a wrapper dep
ChangeThisFile API (Ktor)No FFmpeg install, serverless, simple pipelinesNetwork latency, 25MB free-tier upload limit

Production tips

  • Always set a process timeout. Use process.waitFor(2, TimeUnit.HOURS) instead of bare waitFor(). A corrupt input file can cause FFmpeg to hang indefinitely.
  • Drain stdout and stderr concurrently. If you don't consume FFmpeg's output stream while it's running, the output buffer fills and the process hangs. Run the stream reader in a separate thread or use .redirectErrorStream(true).
  • Use -q:a 2 for VBR encoding. Variable bitrate (-q:a 0–9, lower is better) produces smaller files at equivalent quality compared to CBR. -q:a 2 is roughly equivalent to 190kbps average.
  • Check for multiple audio tracks. Some MP4s (e.g., dubbed films) have multiple audio streams. -map 0:a:0 extracts only the first. -map a extracts all streams into the MP3 (not always what you want).
  • Temp file cleanup. If your pipeline creates temp files for upload, use Kotlin's File.createTempFile() with a try/finally block to guarantee deletion even on exceptions.

For self-hosted JVM services with FFmpeg installed, ProcessBuilder is the lightest approach. ffmpeg-kt adds ergonomics for progress-tracking use cases. For zero-dependency pipelines or quick scripts, the API. Free tier covers 1,000 conversions/month.