Skip to content

Using FFmpegKitNext on Android

Taner Sener edited this page Jul 28, 2026 · 3 revisions

This page shows how to use FFmpegKitNext in an Android application after the Android AAR has already been added to your project.

FFmpegKitNext for Android is implemented in Kotlin and exposes a Java-compatible API. The examples below use Java. Kotlin apps use the same classes with lambdas and Kotlin properties, for example log.message instead of log.getMessage().

1. Import the API

import com.arthenica.ffmpegkit.FFmpegKit;
import com.arthenica.ffmpegkit.FFmpegKitConfig;
import com.arthenica.ffmpegkit.FFprobeKit;
import com.arthenica.ffmpegkit.Packages;

2. Execute FFmpeg

Use execute for a synchronous command. It blocks the calling thread until FFmpeg finishes, so do not call it from the Android main thread for long-running work.

FFmpegSession session = FFmpegKit.execute("-i file1.mp4 -c:v mpeg4 file2.mp4");
ReturnCode returnCode = session.getReturnCode();

if (ReturnCode.isSuccess(returnCode)) {
    // SUCCESS
} else if (ReturnCode.isCancel(returnCode)) {
    // CANCEL
} else {
    Log.d(TAG, String.format(
        "Command failed with state %s and rc %s.%s",
        FFmpegKitConfig.sessionStateToString(session.getState()),
        returnCode,
        session.getFailStackTrace()));
}

For paths, filtergraphs, SAF URLs or user-provided values, prefer argument arrays. They avoid quoting and escaping problems caused by command-string parsing.

String[] arguments = new String[] {
    "-i", inputPath,
    "-vf", "scale=1280:-2",
    "-c:v", "mpeg4",
    outputPath
};

FFmpegSession session = FFmpegKit.executeWithArguments(arguments);

Use async execution for normal Android UI flows. Callbacks are not guaranteed to run on the main thread, so dispatch UI updates with Activity.runOnUiThread, a Handler, coroutines or your app's usual threading mechanism.

FFmpegSession session = FFmpegKit.executeWithArgumentsAsync(arguments, completedSession -> {
    ReturnCode rc = completedSession.getReturnCode();

    if (ReturnCode.isSuccess(rc)) {
        // SUCCESS
    } else {
        Log.d(TAG, completedSession.getAllLogsAsString());
    }
}, log -> {
    Log.d(TAG, log.getMessage());
}, statistics -> {
    Log.d(TAG, String.format(
        "frame=%d time=%f speed=%f",
        statistics.getVideoFrameNumber(),
        statistics.getTime(),
        statistics.getSpeed()));
});

You can also run an async session on a custom executor.

ExecutorService executor = Executors.newSingleThreadExecutor();

FFmpegKit.executeWithArgumentsAsync(arguments, completedSession -> {
    // Handle completion.
}, executor);

Shut down custom executors when your app no longer needs them.

3. Inspect Sessions

Every execute, executeWithArguments, executeAsync and executeWithArgumentsAsync call creates a session.

FFmpegSession session = FFmpegKit.executeWithArguments(arguments);

long sessionId = session.getSessionId();
String command = session.getCommand();
String[] commandArguments = session.getArguments();
SessionState state = session.getState();
ReturnCode returnCode = session.getReturnCode();

Date createTime = session.getCreateTime();
Date startTime = session.getStartTime();
Date endTime = session.getEndTime();
long duration = session.getDuration();

String output = session.getOutput();
String failStackTrace = session.getFailStackTrace();

List<com.arthenica.ffmpegkit.Log> logs = session.getAllLogs();
List<Statistics> statistics = session.getAllStatistics();
Statistics lastStatistics = session.getLastReceivedStatistics();

getLogs() and getLogsAsString() return the log entries already delivered to the session. getAllLogs(), getAllLogsAsString() and getAllStatistics() wait briefly for asynchronous native messages that may still be in transit.

Session history keeps the most recent sessions. The default size is 10; the Android API enforces a hard limit below 1000.

FFmpegKitConfig.setSessionHistorySize(100);

List<Session> allSessions = FFmpegKitConfig.getSessions();
List<FFmpegSession> ffmpegSessions = FFmpegKitConfig.getFFmpegSessions();
List<FFprobeSession> ffprobeSessions = FFmpegKitConfig.getFFprobeSessions();
List<MediaInformationSession> mediaInformationSessions = FFmpegKitConfig.getMediaInformationSessions();
List<Session> runningSessions = FFmpegKitConfig.getSessionsByState(SessionState.RUNNING);

Session lastSession = FFmpegKitConfig.getLastSession();
Session lastCompletedSession = FFmpegKitConfig.getLastCompletedSession();

FFmpegKitConfig.deleteSession(sessionId);
FFmpegKitConfig.clearSessions();

Use FFmpegKit.listSessions() when you only need FFmpeg sessions and FFprobeKit.listFFprobeSessions() / FFprobeKit.listMediaInformationSessions() for FFprobe-related history.

4. Execute FFprobe and Read Media Information

Use FFprobeKit for FFprobe commands.

FFprobeSession session = FFprobeKit.execute(
    "-hide_banner -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 file1.mp4");

if (!ReturnCode.isSuccess(session.getReturnCode())) {
    Log.d(TAG, "FFprobe failed. Check session output for details.");
}

Argument-array execution is available for FFprobe as well.

String[] probeArguments = new String[] {
    "-hide_banner",
    "-print_format", "json",
    "-show_format",
    "-show_streams",
    inputPath
};

FFprobeKit.executeWithArgumentsAsync(probeArguments, completedSession -> {
    Log.d(TAG, completedSession.getOutput());
}, log -> {
    Log.d(TAG, log.getMessage());
});

Use getMediaInformation when you want parsed format, stream and chapter metadata.

MediaInformationSession mediaSession = FFprobeKit.getMediaInformation(inputPath);
MediaInformation information = mediaSession.getMediaInformation();

if (information != null) {
    String duration = information.getDuration();
    String format = information.getFormat();
    String bitrate = information.getBitrate();
    int streamCount = information.getStreams().size();
}

Custom media information commands must print FFprobe JSON output.

MediaInformationSession customSession = FFprobeKit.getMediaInformationFromCommand(
    "-v error -hide_banner -print_format json -show_format -show_streams -i " + inputPath);

Because getMediaInformationFromCommand accepts a command string, quote or escape paths yourself. Use getMediaInformation(path) for ordinary file paths and SAF URLs.

5. Cancel Running Work

Cancel all running FFmpeg sessions:

FFmpegKit.cancel();

Cancel one running FFmpeg session:

FFmpegKit.cancel(sessionId);

You can also cancel through the session object:

session.cancel();

Cancellation is asynchronous. The session callback is still the right place to observe the final state and return code.

6. Use Storage Access Framework URIs

Android content:// URIs from ACTION_OPEN_DOCUMENT, ACTION_CREATE_DOCUMENT and similar pickers cannot be passed directly to FFmpeg. Convert them to ffkitsaf: URLs first.

Uri inputUri = inputIntent.getData();
Uri outputUri = outputIntent.getData();

String inputUrl = FFmpegKitConfig.getSafParameterForRead(context, inputUri);
String outputUrl = FFmpegKitConfig.getSafParameterForWrite(context, outputUri);

FFmpegKit.executeWithArgumentsAsync(new String[] {
    "-i", inputUrl,
    "-c:v", "mpeg4",
    outputUrl
}, completedSession -> {
    // Handle completion.
});

Use a custom open mode when Android's ContentResolver.openFileDescriptor needs one.

String readWriteUrl = FFmpegKitConfig.getSafParameter(context, uri, "rw");

By default, a generated SAF URL is not reusable and is automatically unregistered when FFmpegKit closes the file descriptor. If you need to use the same URL in more than one command, create it as reusable and unregister it yourself after the last command.

String reusableInputUrl = FFmpegKitConfig.getSafParameterForRead(context, inputUri, true);

FFprobeKit.executeWithArgumentsAsync(new String[] {
    "-hide_banner",
    "-show_streams",
    reusableInputUrl
}, firstSession -> {
    FFprobeKit.executeWithArgumentsAsync(new String[] {
        "-hide_banner",
        "-show_format",
        reusableInputUrl
    }, secondSession -> {
        FFmpegKitConfig.unregisterSafProtocolUrl(reusableInputUrl);
    });
});

You can also change the default for newly created SAF URLs:

FFmpegKitConfig.setSafUrlsReusable(true);

When you enable reusable SAF URLs globally, remember that each reusable URL must eventually be released with unregisterSafProtocolUrl.

7. Configure Callbacks, Logs and Concurrency

Global callbacks apply to all matching sessions. Pass null to disable a callback.

FFmpegKitConfig.enableFFmpegSessionCompleteCallback(session -> {
    Log.d(TAG, "FFmpeg session completed: " + session.getSessionId());
});

FFmpegKitConfig.enableFFprobeSessionCompleteCallback(session -> {
    Log.d(TAG, "FFprobe session completed: " + session.getSessionId());
});

FFmpegKitConfig.enableMediaInformationSessionCompleteCallback(session -> {
    Log.d(TAG, "Media information session completed: " + session.getSessionId());
});

FFmpegKitConfig.enableLogCallback(log -> {
    Log.d(TAG, log.getMessage());
});

FFmpegKitConfig.enableStatisticsCallback(statistics -> {
    Log.d(TAG, "time=" + statistics.getTime());
});

Set the active FFmpeg log level when you need more or less native output.

FFmpegKitConfig.setLogLevel(Level.AV_LOG_INFO);

Log and statistics redirection is enabled by default. If you call FFmpegKitConfig.disableRedirection(), log callbacks, statistics callbacks and FFprobeKit.getMediaInformation methods stop working. Use enableRedirection() to turn it back on.

Use log redirection strategies to control Logcat printing when callbacks are or are not registered.

FFmpegKitConfig.setLogRedirectionStrategy(
    LogRedirectionStrategy.PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED);

For one quiet session, create the session explicitly and execute it through FFmpegKitConfig.

FFmpegSession quietSession = FFmpegSession.create(
    arguments,
    completedSession -> {
        // Handle completion.
    },
    null,
    null,
    LogRedirectionStrategy.NEVER_PRINT_LOGS);

FFmpegKitConfig.asyncFFmpegExecute(quietSession);

The default async executor runs up to 10 sessions in parallel. Change the limit if your application needs stricter scheduling.

FFmpegKitConfig.setAsyncConcurrencyLimit(2);

8. Use Fonts and drawtext

The FFmpeg drawtext filter requires an Android artifact that includes freetype and libharfbuzz. If you want to use font family names through fontconfig, the artifact must also include fontconfig. Without fontconfig, use an explicit fontfile path.

String filter = "drawtext=fontfile=" + fontFile.getAbsolutePath() + ":text='Hello':x=20:y=20";

FFmpegKit.executeWithArguments(new String[] {
    "-i", inputPath,
    "-vf", filter,
    outputPath
});

Register Android system fonts and custom font directories when fontconfig is available.

HashMap<String, String> fontNameMapping = new HashMap<>();
fontNameMapping.put("MyFontName", "Doppio One");

FFmpegKitConfig.setFontDirectoryList(
    context,
    Arrays.asList(fontDirectory.getAbsolutePath(), "/system/fonts"),
    fontNameMapping);

FFmpegKit.executeWithArguments(new String[] {
    "-i", inputPath,
    "-vf", "drawtext=font='MyFontName':text='Hello':x=20:y=20",
    outputPath
});

Use Collections.emptyMap() when you do not need custom font-name mappings.

9. Use FFmpegKit Protocols

FFmpegKitNext provides custom protocol helpers for data that is not represented by a normal filesystem path.

  • ffkitsaf: is used by the SAF helpers in section 6.
  • ffkitmem: is a seekable finite in-memory input/output protocol.
  • ffkitstream: is a non-seekable memory-backed streaming input/output protocol.

Use ffkitmem: for byte arrays or direct ByteBuffer inputs and outputs.

FFmpegKitInputBuffer input = FFmpegKitInputBuffer.fromByteArray(inputBytes, "jpg");
FFmpegKitOutputBuffer output = FFmpegKitOutputBuffer.create("jpg");

FFmpegKit.executeWithArgumentsAsync(new String[] {
    "-i", input.getUrl(),
    "-vf", "scale=640:-2",
    output.getUrl()
}, completedSession -> {
    try {
        if (ReturnCode.isSuccess(completedSession.getReturnCode())) {
            byte[] convertedBytes = output.toByteArray();
            long outputSize = output.getSize();
        } else {
            Log.d(TAG, completedSession.getAllLogsAsString());
        }
    } finally {
        input.close();
        output.close();
    }
});

FFmpegKitInputBuffer.fromDirectByteBuffer(...) accepts a direct ByteBuffer. FFmpegKitOutputBuffer.asDirectByteBuffer() returns a read-only direct buffer view of the output.

Use ffkitstream: when data is produced or consumed incrementally. Reads and writes can block, so run them off the Android main thread.

FFmpegKitStreamInput input = FFmpegKitStreamInput.create("raw");

FFmpegKit.executeWithArgumentsAsync(new String[] {
    "-f", "s16le",
    "-ar", "48000",
    "-ac", "2",
    "-i", input.getUrl(),
    outputPath
}, completedSession -> {
    input.close();
});

input.write(pcmChunk, 5000);
input.closeInput();

write returns the number of bytes written. For larger buffers, loop until all bytes are accepted, then call closeInput() to signal EOF.

For streaming output, read until an empty byte array signals end of stream. A null result means the read timed out.

FFmpegKitStreamOutput output = FFmpegKitStreamOutput.create("raw");

FFmpegKit.executeWithArgumentsAsync(new String[] {
    "-i", inputPath,
    "-f", "s16le",
    "-acodec", "pcm_s16le",
    output.getUrl()
}, completedSession -> {
    Log.d(TAG, "Stream output command completed: " + completedSession.getReturnCode());
});

try {
    while (true) {
        byte[] chunk = output.read(4096, 1000);
        if (chunk == null) {
            continue;
        }
        if (chunk.length == 0) {
            break;
        }

        consume(chunk);
    }
} finally {
    output.close();
}

Close every FFmpegKitInputBuffer, FFmpegKitOutputBuffer, FFmpegKitStreamInput and FFmpegKitStreamOutput you create.

10. Use Named Pipes

Named pipes are filesystem FIFOs created under the app cache directory. They are useful when FFmpeg should read from or write to a path while another part of your app streams the bytes.

String pipe = FFmpegKitConfig.registerNewFFmpegPipe(context);
if (pipe == null) {
    throw new IllegalStateException("Failed to create FFmpeg pipe.");
}

FFmpegKit.executeWithArgumentsAsync(new String[] {
    "-i", pipe,
    "-c:v", "mpeg4",
    outputPath
}, completedSession -> {
    FFmpegKitConfig.closeFFmpegPipe(pipe);
});

new Thread(() -> {
    try (FileOutputStream stream = new FileOutputStream(pipe)) {
        stream.write(inputBytes);
    } catch (IOException e) {
        Log.e(TAG, "Writing pipe failed.", e);
    }
}).start();

Always close pipes with closeFFmpegPipe when the command is done.

11. Android-Specific Configuration Helpers

Read version information when reporting diagnostics.

String ffmpegVersion = FFmpegKitConfig.getFFmpegVersion();
String ffmpegKitVersion = FFmpegKitConfig.getVersion();
String buildDate = FFmpegKitConfig.getBuildDate();
String packageName = Packages.getPackageName();

Packages.getPackageName() returns the custom package name configured at build time.

Set native environment variables before running the command that needs them.

FFmpegKitConfig.setEnvironmentVariable("FFREPORT", "file=" + reportFile.getAbsolutePath());

List Android camera IDs supported by FFmpegKit on the current device. An empty list means no supported camera was detected.

List<String> cameraIds = FFmpegKitConfig.getSupportedCameraIds(context);

Ignore a signal when embedding FFmpegKit in frameworks that also manage signals, such as Unity, Xamarin or other Mono-based runtimes.

FFmpegKitConfig.ignoreSignal(Signal.SIGXCPU);

12. Test Applications

The Android Java and Kotlin test applications in the ffmpeg-kit-next-test project show complete UI examples for commands, FFprobe, SAF, ffkitmem, pipes, fonts, concurrent execution and statistics.

Clone this wiki locally