-
Notifications
You must be signed in to change notification settings - Fork 25
Using FFmpegKitNext on Flutter
This page shows how to use FFmpegKitNext in a Flutter application after the ffmpeg_kit_next_flutter plugin and its native binaries have already been added to the app.
The plugin supports Android, iOS, iPadOS, macOS and Linux. Android-only APIs are marked below.
Import only the classes your app uses.
import 'package:ffmpeg_kit_next_flutter/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_next_flutter/ffmpeg_kit_config.dart';
import 'package:ffmpeg_kit_next_flutter/ffprobe_kit.dart';
import 'package:ffmpeg_kit_next_flutter/packages.dart';Most API calls initialize the plugin automatically. If you want to suppress the native load confirmation, initialize before the first FFmpegKit call.
await FFmpegKitConfig.init(printLoadConfirmation: false);execute returns a Future<FFmpegSession> that completes when the command finishes.
final session = await FFmpegKit.execute('-i file1.mp4 -c:v mpeg4 file2.mp4');
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
// SUCCESS
} else if (ReturnCode.isCancel(returnCode)) {
// CANCEL
} else {
final state = FFmpegKitConfig.sessionStateToString(await session.getState());
final output = await session.getAllLogsAsString();
final failStackTrace = await session.getFailStackTrace();
print('FFmpeg failed with state $state and rc $returnCode.');
print(output);
print(failStackTrace);
}For paths, filtergraphs, SAF URLs or user-provided values, prefer argument arrays. They avoid quoting and escaping problems caused by command-string parsing.
final session = await FFmpegKit.executeWithArguments([
'-i', inputPath,
'-vf', 'scale=1280:-2',
'-c:v', 'mpeg4',
outputPath,
]);Use executeAsync or executeWithArgumentsAsync for normal UI flows. The method returns after the native session is scheduled; the complete callback receives the final session.
final session = await FFmpegKit.executeWithArgumentsAsync([
'-i', inputPath,
'-vf', 'scale=1280:-2',
'-c:v', 'mpeg4',
outputPath,
], (FFmpegSession completedSession) async {
final rc = await completedSession.getReturnCode();
if (ReturnCode.isSuccess(rc)) {
// SUCCESS
} else {
print(await completedSession.getAllLogsAsString());
}
}, (Log log) {
print(log.getMessage());
}, (Statistics statistics) {
print('frame=${statistics.getVideoFrameNumber()} time=${statistics.getTime()} speed=${statistics.getSpeed()}');
});
print('Started FFmpeg session ${session.getSessionId()}.');Every FFmpeg, FFprobe and media-information execution creates a session.
final session = await FFmpegKit.executeWithArguments(arguments);
final sessionId = session.getSessionId();
final command = session.getCommand();
final commandArguments = session.getArguments();
final state = await session.getState();
final returnCode = await session.getReturnCode();
final createTime = session.getCreateTime();
final startTime = session.getStartTime();
final endTime = await session.getEndTime();
final duration = await session.getDuration();
final output = await session.getOutput();
final failStackTrace = await session.getFailStackTrace();
final logs = await session.getAllLogs();
final statistics = await session.getAllStatistics();
final lastStatistics = await 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 recent sessions. Set the history size before starting heavy batches if you need more or fewer entries.
await FFmpegKitConfig.setSessionHistorySize(100);
final allSessions = await FFmpegKitConfig.getSessions();
final ffmpegSessions = await FFmpegKitConfig.getFFmpegSessions();
final ffprobeSessions = await FFmpegKitConfig.getFFprobeSessions();
final mediaInformationSessions = await FFmpegKitConfig.getMediaInformationSessions();
final runningSessions = await FFmpegKitConfig.getSessionsByState(SessionState.running);
final lastSession = await FFmpegKitConfig.getLastSession();
final lastCompletedSession = await FFmpegKitConfig.getLastCompletedSession();
if (sessionId != null) {
await FFmpegKitConfig.deleteSession(sessionId);
}
await FFmpegKitConfig.clearSessions();Use FFmpegKit.listSessions() when you only need FFmpeg sessions and FFprobeKit.listFFprobeSessions() / FFprobeKit.listMediaInformationSessions() for FFprobe-related history.
Use FFprobeKit for FFprobe commands.
final session = await FFprobeKit.execute(
'-hide_banner -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 file1.mp4',
);
if (!ReturnCode.isSuccess(await session.getReturnCode())) {
print(await session.getOutput());
}Argument-array execution is available for FFprobe as well.
await FFprobeKit.executeWithArgumentsAsync([
'-hide_banner',
'-print_format', 'json',
'-show_format',
'-show_streams',
inputPath,
], (FFprobeSession completedSession) async {
print(await completedSession.getOutput());
}, (Log log) {
print(log.getMessage());
});Use getMediaInformation when you want parsed format, stream and chapter metadata.
final mediaSession = await FFprobeKit.getMediaInformation(inputPath);
final information = mediaSession.getMediaInformation();
if (information != null) {
final duration = information.getDuration();
final format = information.getFormat();
final bitrate = information.getBitrate();
final streamCount = information.getStreams().length;
}Custom media information commands must print FFprobe JSON output.
final customSession = await FFprobeKit.getMediaInformationFromCommandArguments([
'-v', 'error',
'-hide_banner',
'-print_format', 'json',
'-show_format',
'-show_streams',
'-i', inputPath,
]);Prefer getMediaInformation(path) or getMediaInformationFromCommandArguments(...) for paths and URLs. If you use getMediaInformationFromCommand(...), quote or escape paths yourself.
Cancel all running FFmpeg sessions:
await FFmpegKit.cancel();Cancel one running FFmpeg session:
await FFmpegKit.cancel(sessionId);You can also cancel through a session object:
await session.cancel();Cancellation is asynchronous. The session callback is still the right place to observe the final state and return code.
Android content:// URIs from the system picker cannot be passed directly to FFmpeg. Convert them to ffkitsaf: URLs first.
final uri = await FFmpegKitConfig.selectDocumentForRead('*/*', [
'image/*',
'video/*',
'audio/*',
]);
if (uri != null) {
final inputUrl = await FFmpegKitConfig.getSafParameterForRead(uri);
if (inputUrl != null) {
await FFmpegKit.executeWithArgumentsAsync([
'-i', inputUrl,
'-c:v', 'mpeg4',
outputPath,
], (session) async {
print(await session.getReturnCode());
});
}
}Create a write URL with the Android document picker:
final uri = await FFmpegKitConfig.selectDocumentForWrite('video.mp4', 'video/*');
if (uri != null) {
final outputUrl = await FFmpegKitConfig.getSafParameterForWrite(uri);
if (outputUrl != null) {
await FFmpegKit.executeWithArguments([
'-i', inputPath,
'-c:v', 'mpeg4',
outputUrl,
]);
}
}Use a custom open mode when Android's ContentResolver.openFileDescriptor needs one.
final readWriteUrl = await FFmpegKitConfig.getSafParameter(uri, 'rw');By default, a generated SAF URL is not reusable and is released automatically after the native file descriptor is closed. If you need to use the same URL in more than one command, create it with the reusable flag set to true and unregister it yourself after the last command.
final reusableInputUrl = await FFmpegKitConfig.getSafParameterForRead(uri, true);
if (reusableInputUrl != null) {
try {
await FFprobeKit.executeWithArguments([
'-hide_banner',
'-show_streams',
reusableInputUrl,
]);
await FFprobeKit.executeWithArguments([
'-hide_banner',
'-show_format',
reusableInputUrl,
]);
} finally {
await FFmpegKitConfig.unregisterSafProtocolUrl(reusableInputUrl);
}
}The SAF picker and SAF URL conversion methods are Android-only.
Global callbacks apply to all matching sessions. Pass no argument or null to disable a callback.
FFmpegKitConfig.enableFFmpegSessionCompleteCallback((session) async {
print('FFmpeg session completed: ${session.getSessionId()}');
});
FFmpegKitConfig.enableFFprobeSessionCompleteCallback((session) async {
print('FFprobe session completed: ${session.getSessionId()}');
});
FFmpegKitConfig.enableMediaInformationSessionCompleteCallback((session) async {
print('Media information session completed: ${session.getSessionId()}');
});
FFmpegKitConfig.enableLogCallback((log) {
print(log.getMessage());
});
FFmpegKitConfig.enableStatisticsCallback((statistics) {
print('time=${statistics.getTime()} size=${statistics.getSize()}');
});Set the active FFmpeg log level when you need more or less native output.
await FFmpegKitConfig.setLogLevel(Level.avLogInfo);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.
await FFmpegKitConfig.disableRedirection();
await FFmpegKitConfig.enableRedirection();Flutter also exposes independent switches for log and statistics callback delivery.
await FFmpegKitConfig.disableLogs();
await FFmpegKitConfig.enableLogs();
await FFmpegKitConfig.disableStatistics();
await FFmpegKitConfig.enableStatistics();Use log redirection strategies to control native console printing when callbacks are or are not registered.
FFmpegKitConfig.setLogRedirectionStrategy(
LogRedirectionStrategy.printLogsWhenNoCallbacksDefined,
);For one quiet session, create the session explicitly and execute it through FFmpegKitConfig.
final quietSession = await FFprobeSession.create(
FFmpegKitConfig.parseArguments(ffprobeCommand),
(session) async {
print(await session.getReturnCode());
},
null,
LogRedirectionStrategy.neverPrintLogs,
);
await FFmpegKitConfig.asyncFFprobeExecute(quietSession);Flutter API calls can also be made from Dart isolates. Initialize the plugin in the isolate before issuing FFmpegKit calls when you need to choose the native load-confirmation behavior explicitly.
await FFmpegKitConfig.init(printLoadConfirmation: false);
final session = await FFmpegKit.execute('-version');The FFmpeg drawtext filter requires native binaries that include freetype and libharfbuzz. If you want to use font family names through fontconfig, the native binaries must also include fontconfig. Without fontconfig, use an explicit fontfile path.
await FFmpegKit.executeWithArguments([
'-i', inputPath,
'-vf', "drawtext=fontfile=$fontFilePath:text='Hello':x=20:y=20",
outputPath,
]);Register platform font directories and custom font-name mappings when fontconfig is available.
await FFmpegKitConfig.setFontDirectoryList([
customFontDirectory,
'/system/fonts',
'/System/Library/Fonts',
], {
'MyFontName': 'Doppio One',
});
await FFmpegKit.executeWithArguments([
'-i', inputPath,
'-vf', "drawtext=font='MyFontName':text='Hello':x=20:y=20",
outputPath,
]);Use setFontDirectory(path) for a single directory.
FFmpegKitNext provides custom protocol helpers for data that is not represented by a normal filesystem path.
-
ffkitsaf:is used by the Android 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-array inputs and outputs.
final input = await FFmpegKitInputBuffer.fromByteArray(inputBytes, 'jpg');
final output = await FFmpegKitOutputBuffer.create(extension: 'jpg');
await FFmpegKit.executeWithArgumentsAsync([
'-i', input.getUrl(),
'-vf', 'scale=640:-2',
output.getUrl(),
], (session) async {
try {
if (ReturnCode.isSuccess(await session.getReturnCode())) {
final convertedBytes = await output.toByteArray();
final outputSize = await output.getSize();
print('Converted ${convertedBytes.length} bytes, stored $outputSize bytes.');
} else {
print(await session.getAllLogsAsString());
}
} finally {
await input.close();
await output.close();
}
});Use ffkitstream: when data is produced or consumed incrementally. Stream reads and writes can wait for native buffers, so do not do heavy stream loops in a performance-sensitive UI path.
final input = await FFmpegKitStreamInput.create(extension: 'raw');
await FFmpegKit.executeWithArgumentsAsync([
'-f', 's16le',
'-ar', '48000',
'-ac', '2',
'-i', input.getUrl(),
outputPath,
], (session) async {
await input.close();
});
final written = await input.write(pcmChunk, timeoutMs: 5000);
if (written != pcmChunk.length) {
// Write the remaining bytes before closing the input.
}
await input.closeInput();For streaming output, read until an empty list signals end of stream.
final output = await FFmpegKitStreamOutput.create(extension: 'raw');
await FFmpegKit.executeWithArgumentsAsync([
'-i', inputPath,
'-f', 's16le',
'-acodec', 'pcm_s16le',
output.getUrl(),
], (session) async {
print('Stream output command completed: ${await session.getReturnCode()}');
});
try {
while (true) {
final chunk = await output.read(4096);
if (chunk.isEmpty) {
break;
}
consume(chunk);
}
} finally {
await output.close();
}Close every FFmpegKitInputBuffer, FFmpegKitOutputBuffer, FFmpegKitStreamInput and FFmpegKitStreamOutput you create.
Named pipes are useful when FFmpeg should read from or write to a path while another part of the app supplies the bytes.
final pipe = await FFmpegKitConfig.registerNewFFmpegPipe();
if (pipe == null) {
throw StateError('Failed to create FFmpeg pipe.');
}
await FFmpegKit.executeWithArgumentsAsync([
'-i', pipe,
'-c:v', 'mpeg4',
outputPath,
], (session) async {
await FFmpegKitConfig.closeFFmpegPipe(pipe);
});
await FFmpegKitConfig.writeToPipe(inputPath, pipe);Always close pipes with closeFFmpegPipe when the command is done.
Read version and package information when reporting diagnostics.
final platform = await FFmpegKitConfig.getPlatform();
final ffmpegVersion = await FFmpegKitConfig.getFFmpegVersion();
final ffmpegKitVersion = await FFmpegKitConfig.getVersion();
final buildDate = await FFmpegKitConfig.getBuildDate();
final packageName = await Packages.getPackageName();
final externalLibraries = await Packages.getExternalLibraries();Packages.getPackageName() returns the custom package name configured at build time.
Set native environment variables before running the command that needs them.
await FFmpegKitConfig.setEnvironmentVariable('FFREPORT', 'file=$reportPath');List Android camera IDs supported by FFmpegKit on the current Android device. This method is Android-only.
final cameraIds = await FFmpegKitConfig.getSupportedCameraIds();Ignore a signal when embedding FFmpegKit in frameworks that also manage signals, such as Unity, Xamarin or other Mono-based runtimes.
await FFmpegKitConfig.ignoreSignal(Signal.sigXCpu);The Flutter test applications in the ffmpeg-kit-next-test project show complete UI examples for commands, FFprobe, SAF, ffkitmem, pipes, fonts, concurrent execution and statistics.
Copyright (c) 2026 FFmpegKitNext
- Status
- Versions
- Changelog
- Project Layout
- Using
- Building
- External Libraries
- Patents
- License