FlutterFlow Video Compression Without a Native Package

You added video_trimmer to shrink a 200 MB upload, and the build died during dependency resolution, before FlutterFlow ever touched the video. So you tried video_editor_2, which died on an Android SDK version instead. Then you went looking for ffmpeg_kit_flutter and found out it's retired.
Quick answer: FlutterFlow video compression fails on native packages because FFmpegKit is retired and the remaining Flutter wrappers collide with FlutterFlow's pinnedintlversion or demand a higher AndroidminSdkVersionthan their docs claim. The DIY path is an on-device compression package, which forces a full native build and adds roughly 9 MB of binary per ABI to your app. The clean path is a FlutterFlow Custom Action that POSTs the file's storage URL to a video API like FFmpeg Micro, then writes the compressed URL back to Firestore or Supabase. That action is pure Dart and HTTP, so it needs no native build and ships no binaries.
Why native video packages break in FlutterFlow
Native video packages break in FlutterFlow for three unrelated reasons that all land in your build log. None are FFmpeg syntax problems, which is why searching for FFmpeg answers gets you nowhere.
The first is intl. FlutterFlow pins intl to match the flutter_localizations version it bundles, and video_trimmer carries its own constraint on the same package. When FlutterFlow bumps its version, pub version solving fails and your project stops compiling. You didn't change anything. The editor did.
The second is minSdkVersion. FFmpegKit's Android artifacts declare API 24, FlutterFlow projects ship lower by default, and the Gradle manifest merger stops the build with uses-sdk:minSdkVersion 21 cannot be smaller than version 24 declared in library. video_editor_2 only hits this on the export path, which is why it looks like it works until a user presses save.
The third is the one that ends the argument. Arthenica retired FFmpegKit, the prebuilt release binaries now return 404, and the migration notice lives in issue #1099 on the arthenica/ffmpeg-kit repo alongside Taner Sener's "Saying Goodbye to FFmpegKit" post. A community fork exists but ships as source, not as a binary you can drop into a pubspec. If a 2024 tutorial told you to add ffmpeg_kit_flutter_min_gpl, that tutorial no longer runs.
Even when a native route does build, you're carrying weight: the "min" FFmpegKit build was about 9 MB per architecture. An Android App Bundle splits that per device, but universal APKs and CI artifacts carry every ABI, and iOS carries it outright.
What compressing a video in FlutterFlow actually has to do
Compressing a phone upload is two decisions: how small the picture gets, and how aggressive the encoder is. Everything else is defaults.
Apple publishes the numbers in Settings under Record Video: 1080p30 is about 65 MB per minute, 1080p60 about 90 MB, 4K30 about 190 MB, and 4K60 about 400 MB. So a two-minute 4K clip from a user's camera roll arrives at roughly 380 MB, the file your app is pushing over cellular into Firebase Storage.
The command that fixes it, the one you'd run on a laptop:
ffmpeg -i input.mov \
-vf "scale='min(1280,iw)':-2" \
-c:v libx264 -preset veryfast -crf 26 -pix_fmt yuv420p \
-c:a aac -b:a 96k \
-movflags +faststart output.mp4
That lands a two-minute clip in the 20 to 30 MB range, roughly a 90% reduction, at a quality nobody complains about on a phone screen. scale='min(1280,iw)':-2 caps width at 1280 without upscaling smaller sources and keeps both dimensions even, which H.264 requires. -movflags +faststart moves the moov atom to the front so the file starts playing before it finishes downloading. Skip that flag and your in-app player buffers the whole file first.
The only question is where that command runs. It doesn't have to be the phone.
The Custom Action that replaces the native package
A FlutterFlow Custom Action is plain Dart, so an action that does nothing but HTTP works in Run mode and Test mode without a local native build. That's the whole trick: you're not adding a plugin with platform channels, you're adding a function that makes a request.
- Upload the original to Firebase Storage or Supabase Storage with FlutterFlow's normal upload action, and keep the download URL in page state.
- Create a Custom Action in Custom Code that POSTs that URL to the API with your compression arguments.
- Store the returned URL on the Firestore document or Supabase row, replacing the original field.
- Delete the original from storage once the compressed copy is confirmed.
// Custom Action: compressVideo
// Pubspec dependency: http: ^1.2.0
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<String?> compressVideo(String sourceUrl) async {
final apiKey = FFAppState().ffmpegMicroKey; // App State, not hardcoded
// Job body follows the FFmpeg Micro docs schema.
final res = await http.post(
Uri.parse('https://api.ffmpeg-micro.com/v1/jobs'),
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
body: jsonEncode({
'input_url': sourceUrl,
'args': "-vf scale='min(1280,iw)':-2 -c:v libx264 -preset veryfast "
"-crf 26 -c:a aac -b:a 96k -movflags +faststart",
'output_format': 'mp4',
}),
);
if (res.statusCode >= 400) {
print('compressVideo failed ${res.statusCode}: ${res.body}');
return null;
}
return jsonDecode(res.body)['id'] as String;
}
That's the entire native-package replacement. FFmpeg Micro runs the encode on its own machines and hands back a URL, and nothing in that action touches your Gradle config or your Podfile. The FFmpeg API overview covers the job schema.
For a 380 MB source, don't leave the app sitting on an open connection. Register a webhook on the job, point it at a Cloud Function or Supabase Edge Function that writes the finished URL into the document, and let the FlutterFlow page listen to it. The reliable pipeline patterns for long-running video jobs post covers the retry and idempotency side.
Trimming video in FlutterFlow without video_trimmer
Trimming in FlutterFlow splits into a UI problem and a processing problem, and only the UI half belongs on the device. Build the in and out points with a native FlutterFlow RangeSlider over the video's duration, available from the video player widget. You get two doubles. Send them.
-ss 4.2 -to 19.8 -c copy
Stream copy makes the trim nearly instant because nothing re-encodes, but -c copy can only cut on keyframes, so your clip may start a couple of seconds early. If you need frame-accurate edges, re-encode the trim instead of copying, and remember filtering and streamcopy can't be used together in the same output. Trim and compress in one job by dropping -c copy and keeping the libx264 arguments from above.
On-device, a backend builder, or one API call
The FlutterFlow community's accepted answer is to upload the raw file to a temporary bucket, compress it in a Buildship flow running FFmpeg, then move the result into the real bucket. It works. It's also three moving parts, a second bucket, and a container you own.
| Approach | What breaks | App size impact | Where it runs |
|---|---|---|---|
| Native package (`video_trimmer`, `video_editor_2`) | `intl` solving, `minSdkVersion` 24, retired FFmpegKit | +9 MB per ABI | User's phone, drains battery |
| Backend builder flow with FFmpeg | Function timeouts on long files, temp bucket cleanup, your ops | None | Your container |
| Custom Action to a video API | Network errors, which you already handle | None | Managed, no servers to run |
The middle row is not wrong, it's just work you're volunteering for. Most serverless function tiers cut off well before a 380 MB 4K encode finishes, which is why the temp bucket exists in that workaround.
Common pitfalls
Firebase Storage download URLs carry a ?alt=media&token= query string, and if you strip it or pass the gs:// path instead, the API gets a 403 on fetch and the job fails at download, not at encode. Pass the full URL exactly as FlutterFlow returns it. Supabase buckets that aren't public need a signed URL with enough TTL to cover the job, and the same presigned URL pattern that works for S3 applies here.
Don't hardcode your API key in the Custom Action. FlutterFlow custom code ships inside the app binary, and anyone can pull strings from an APK. Keep the key server-side in your webhook handler, or gate job creation behind a Cloud Function that the app calls with its Firebase Auth token.
Watch the audio. Phones record AAC already, so -c:a copy saves time, but screen recordings and some Android OEM cameras produce audio that a straight copy will desync. Re-encoding to -b:a 96k costs almost nothing and removes the problem.
Test the action on a real device build before you ship. Run mode uses your development machine's network stack, and a request that works there can still hit an Android cleartext-traffic block or a missing iOS entitlement on device.
Finally, don't delete the original until the compressed URL is written and readable. A webhook that fires before your storage write commits leaves users with a document pointing at a file that no longer exists.
When offloading compression is the wrong call
Sending video to an API is the wrong call when the video can't leave the device. Apps under health or legal data constraints, or offline-first apps where users record in the field with no connection, need a hardware-backed on-device compression package instead, limited to resize and re-encode with no filter graph. That's a real trade and it's fine to make it.
Offloading is also overkill for small files. If your uploads are already under about 10 MB, the network round trip costs more than the bytes you'd save. And if what you want is a scrubbing timeline with live preview inside the app, that's a video editor, not a processing API, and no HTTP call gives you frame-accurate playback on the canvas.
FAQ
Can FlutterFlow run FFmpeg on the device?
FlutterFlow can't practically run FFmpeg on the device anymore, because FFmpegKit and its Flutter binding are retired and the prebuilt binaries return 404. The supported path is a Custom Action that sends the video's storage URL to a cloud video API and writes the result back to your database.
Do I need a native build to use a Custom Action for compression?
A Custom Action that only makes HTTP requests needs no native build and runs in FlutterFlow's Run mode and Test mode, because it's pure Dart with no platform channels. Native builds are only required when a package includes Android or iOS native code, which is what made video_trimmer and video_editor_2 painful.
How do I trim a video in FlutterFlow without video_trimmer?
To trim a video in FlutterFlow without video_trimmer, build the in and out points with a RangeSlider over the video player's duration, then pass those two timestamps to a video API as -ss and -to in a Custom Action. The device handles selection only, and the cut happens server-side.
Why does video_editor_2 fail on Android when the docs say minSdk 21?
video_editor_2 fails on Android because its FFmpeg export path pulls in FFmpegKit artifacts that declare minSdkVersion 24, higher than the value in the package docs, and Gradle's manifest merger refuses to build. Raising your project's minimum SDK to 24 clears that error but drops older devices and still leaves you with a retired dependency.
What size should I compress video to before uploading from a mobile app?
For mobile uploads, 1280 pixels wide at CRF 26 with 96 kbps AAC audio is the practical target, landing a two-minute 4K clip around 20 to 30 MB from an original near 380 MB. That survives cellular uploads and still looks clean on any phone screen.
Wire the Custom Action once, point it at your Firebase or Supabase URL, and the compression problem stops being a build problem. The free tier is enough to run the action end to end and watch the first compressed file land in Firestore: sign up free.
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

Expo Video Compression Isn't a Camera Setting. Do It on Upload.
expo-camera records ~95 MB per minute at 720p with no bitrate or fps control. Expo video compression options compared, plus the pipeline that fixes it.

Compress Video for Discord: Hit 10, 50, or 500 MB on Purpose
Discord's limit is 10 MB free, 50 MB Nitro Basic, 500 MB Nitro. Compress video for Discord by computing bitrate from duration, then two-pass encode to hit it.

The Telegram Bot Video Size Limit Isn't 50 MB. It's Three Caps.
The telegram bot video size limit is 50 MB, or 20 MB by URL. Measure with ffprobe, compute a bitrate budget for a 48 MB ceiling, then re-encode or split.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free