FFmpeg Flutter

Handling Video with FFmpeg in Flutter

Video processing is one of the most resource-intensive tasks, especially in terms of RAM and CPU usage. Without proper optimization, your application can easily crash. During development, I encountered issues when using third-party libraries from Pub.dev to:

  • Convert videos into GIFs.

  • Convert a list of images into a video.

These libraries often take a long time to process and produce results that do not meet the desired quality. However, with FFmpeg, you can efficiently solve these issues.

Introduction to FFmpeg

FFmpeg is a powerful open-source library designed for audio and video processing. FFmpeg supports various tasks such as:

  • Format conversion (MP4, MKV, MP3, etc.).

  • Video and audio processing: Adding watermarks, trimming, merging, and compressing.

  • Creating GIFs from videos.

  • Streaming and media optimization.

The ffmpeg_kit_flutter_full_gpl package brings the full capabilities of FFmpeg to Flutter. Additionally, you can explore other packages at FFmpeg Kit Packages.

Installing the FFmpeg Toolkit

Note: This guide is based on Flutter 3.24.5 and ffmpeg_kit_flutter_full_gpl 6.0.3. It appears that newer versions of Flutter may not work well with this library.

Installation

Add the dependency to your pubspec.yaml file:

ffmpeg_kit_flutter_full_gpl: ^latest_version

Run the following command:

flutter pub get

Android Configuration

Add the following to your android/app/build.gradle file:

defaultConfig {
    ...
    minSdk = 24
    ...
}

Update permissions in AndroidManifest.xml:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>

iOS Configuration

Add the following to your Info.plist file:

<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to pick files.</string>
<key>NSDocumentDirectoryUsageDescription</key>
<string>We need access to your documents to pick files.</string>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

Ensure that your ios/Podfile specifies the platform version:

platform :ios, '13.0'

Convert a Sequence of Images to Video

Below is a function to convert a list of images into a video. My idea is to store all images in a directory, and FFmpegKit will read them to create a video.

Future<({bool isSuccess, String message})> convertImageDirectoryToVideo({
  required String imagesPath,
  required String outputVideoPath,
  int framerate = 24,
  int? fps,
  int? quality,
}) async {
  var isSuccess = false;
  var message = "";

  final command = '-framerate $framerate -i $imagesPath/%d.png '
      '${fps != null ? "-r $fps" : ""} '
      '${quality != null ? "-crf $quality -preset slow" : ""} '
      '-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" '
      '-c:v libx264 -pix_fmt yuv420p -movflags +faststart $outputVideoPath';

  await FFmpegKit.execute(command).then((session) async {
    final returnCode = await session.getReturnCode();

    if (returnCode?.isValueSuccess() ?? false) {
      message = 'Video conversion successful!';
      isSuccess = true;
    } else {
      final output = await session.getOutput();
      message = 'Video conversion failed: $output';
    }
  });

  return (isSuccess: isSuccess, message: message);
}

Example Usage:

final result = convertImageDirectoryToVideo(
  imagesPath: "path/to/images",
  outputVideoPath: "path/to/videoOutput.mp4",
);

Ensure that the directory path/to/images contains images named with sequential numbers and in .png format, as illustrated in the example:

Convert a Sequence of Images to Video

The output file path/to/videoOutput.mp4 must have a unique name to avoid errors during FFmpeg processing. In the command above, the images will be automatically resized to match an even-numbered resolution. You can modify the command as needed.

Add a Watermark to a Video

Function to Add Watermark

Future<({bool isSuccess, String message})> addWatermarkToVideo({
  required String videoPath,
  required String watermarkPath,
  required String outputPath,
  required int x,
  required int y,
  int? width,
  int? height,
}) async {
  var isSuccess = false;
  var message = "";

  final scaleFilter = (width != null && height != null)
      ? "[1:v]scale=$width:$height[wm];[0:v][wm]overlay=$x:$y"
      : "overlay=$x:$y";
  final command =
      '-i $videoPath -i $watermarkPath -filter_complex "$scaleFilter" -codec:a copy $outputPath';

  await FFmpegKit.execute(command).then((session) async {
    final returnCode = await session.getReturnCode();
    if (returnCode?.isValueSuccess() ?? false) {
      message = "Watermark added successfully!";
      isSuccess = true;
    } else {
      message = "Failed to add watermark.";
    }
  });

  return (isSuccess: isSuccess, message: message);
}

Example Usage

final result = addWatermarkToVideo(
  videoPath: videoPath!,
  watermarkPath: watermarkPath!,
  outputPath: outputPath,
  x: 20,
  y: 30,
  width: 200,
  height: 200,
);
  • x, y: Coordinates of the watermark.

  • width, height: Dimensions of the watermark (optional).

Reduce Video Quality

Function to Reduce Video Quality

Future<({bool isSuccess, String message})> reduceVideoQualityByPercentage({
  required String inputPath,
  required String outputPath,
  required double qualityPercentage
}) async {
  final crfValue = (51 - 18) * (1 - qualityPercentage / 100) + 18;
  final command =
      '-i $inputPath -crf ${crfValue.toInt()} -preset fast -codec:a copy $outputPath';

  var isSuccess = false;
  var message = "";
  await FFmpegKit.execute(command).then((session) async {
    final returnCode = await session.getReturnCode();
    if (returnCode?.isValueSuccess() ?? false) {
      message = "Quality reduced successfully!";
      isSuccess = true;
    } else {
      message = "Failed to reduce quality.";
    }
  });

  return (isSuccess: isSuccess, message: message);
}

Example Usage

final result = reduceVideoQualityByPercentage(
  inputPath: videoPath!,
  outputPath: outputPath,
  qualityPercentage: 50,
);
  • qualityPercentage: The percentage of the original quality to retain. A lower percentage means higher compression.

  • crfValue: Controls the quality, where lower values mean better quality and higher values mean more compression.

Create GIF from Video

Function to Create GIF

Future<({bool isSuccess, String message})> createGifFromVideo({
  required String inputPath,
  required String outputPath,
  required double fps,
  required int quality,
  int scale = 320,
}) async {
  final command =
      '-i $inputPath -vf "fps=$fps,scale=$scale:-1:flags=lanczos" -q:v $quality $outputPath';

  var isSuccess = false;
  var message = "";
  await FFmpegKit.execute(command).then((session) async {
    final returnCode = await session.getReturnCode();
    if (returnCode?.isValueSuccess() ?? false) {
      message = "GIF created successfully!";
      isSuccess = true;
    } else {
      message = "Failed to create GIF.";
    }
  });

  return (isSuccess: isSuccess, message: message);
}

Example Usage

final result = createGifFromVideo(
  inputPath: videoPath!,
  outputPath: pathGif,
  fps: 10,
  quality: 10,
  scale: 500,
);
  • fps: Frame rate of the GIF (higher values make the GIF smoother but increase file size).

  • quality: Lower values mean better quality but larger file size.

  • scale: The width of the GIF; the height is adjusted automatically to maintain the aspect ratio.

âš  Note:

  • The larger the scale, the closer the GIF quality is to the original video.

  • Be mindful of RAM usage when using this function, as GIF creation can be memory-intensive.

If you want a ready-to-use solution, try my plugin: https://pub.dev/packages/my_maker_video.

Buy Me a Coffee | Support Me on Ko-fi

Last updated