flutter_video_editor_plugin
Timeline video editor for Flutter apps, backed by the native Infinive SDKs for iOS and Android. VideoEditor.present() opens the native editor and returns the rendered MP4.
A license key is required. Get a trial key for debug builds. Full documentation: infinive.dev/docs.
Features
| Area | Included |
|---|---|
| Sources | Videos and photos from the gallery; photos become image clips |
| Timeline | Pinch zoom, scrubbing, trim, split, delete, per-clip volume |
| Overlays | Text (fonts, colors, styles); GIPHY stickers |
| Look | Color filters, video effects, clip transitions |
| Audio | Background music with waveform, section adjustment, volume; optional host audio picker |
| Editing | Undo and redo up to 50 steps; drafts saved on exit |
| Export | MP4 (H.264 or HEVC) in 9:16, 1:1, 4:5, 3:4 or 16:9; cover frame or photo |
| Analytics | Usage stats in the result, for your own analytics |
| Localization | English and Korean; strings overridable from native resources |
Install
dependencies:
flutter_video_editor_plugin: ^1.2.5
The native SDKs are resolved by name (VideoEditorCore pod, show.infini:videoeditor Maven artifact). The plugin adds the Maven repository to the Android build.
Android
minSdk24.- Launch the editor from a
FlutterActivity. - No manifest changes.
iOS
platform :ios, '16.0'in the Podfile.Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo access is needed to pick videos to edit.</string>
The editor asks for full photo-library access right before it opens the picker. If the user denies or limits access, it shows a Settings button; with no clips, present() returns null. initialSource doesn't need the permission.
Present the editor
import 'package:flutter_video_editor_plugin/flutter_video_editor_plugin.dart';
final result = await VideoEditor.present(
initialSource: pickedPath, // optional; omit to open the gallery
config: const VideoEditorConfig(licenseKey: 've_live_…', giphyApiKey: '…'),
onFailure: (failure) => log('editor failed: $failure'),
);
if (result != null) {
upload(result.videoPath);
}
null means the user closed the editor. The editor is a native screen over the current one, not a Flutter route.
initialSource is a local file path, or a content:// URI on Android. A photo becomes a one-second image clip; on Android, a photo path needs its file extension.
Result
| Field | Value |
|---|---|
videoPath |
Rendered MP4 in the app cache. Move, upload or delete it. |
coverPath |
Cover JPEG in the app cache. Empty if no cover could be extracted (coverUnavailable arrives first). |
durationSec |
Duration in seconds. |
audioPaths |
Audio tracks used, in timeline order, as pickAudio or the system picker returned them. |
draftId |
Draft holding this edit; null if drafts are off or nothing was saved. See "Resume drafts". |
usage |
Usage stats. See "Read usage data". |
VideoEditorScreen, the route wrapper, is deprecated and will be removed in the next major version.
Configure
VideoEditor.present(
config: VideoEditorConfig(
licenseKey: 've_live_…',
giphyApiKey: myGiphyKey,
textFonts: const [TextFontSpec(displayName: 'Brand', asset: 'fonts/Brand-Bold.ttf')],
minDurationSeconds: 3,
maxDurationSeconds: 60,
locale: Localizations.localeOf(context).toLanguageTag(),
draftsEnabled: true,
coverPickerEnabled: true,
),
output: const RenderOutputConfig(aspectRatio: RenderAspectRatio.ratio1x1, bitrate: 5000000),
);
VideoEditorConfig |
|
|---|---|
licenseKey |
Required. |
giphyApiKey |
Sticker tool is disabled without it. |
textFonts |
Replaces the bundled fonts. null keeps them. See "Add fonts". |
minDurationSeconds, maxDurationSeconds |
Exportable length. Checked on "Next"; editing isn't restricted. null for no limit. |
allowAddClip |
false hides the add button and keeps the last clip. Default true. |
locale |
BCP 47 tag. Only needed if your app has its own language setting. |
draftsEnabled |
See "Resume drafts". Default false. |
coverPickerEnabled |
See "Let the user pick a cover". Default false. |
detachedRenderEnabled |
See "Show render progress in your own UI". Default false. |
Invalid values (empty licenseKey, minimum above maximum, unknown codec) are rejected with a bad_arguments PlatformException.
Output
RenderOutputConfig overrides only the fields you set. Defaults: 9:16, 7 Mbps cap, 30 fps cap, H.264.
RenderAspectRatio |
Size |
|---|---|
ratio9x16 (default) |
1080×1920 |
ratio1x1 |
1080×1080 |
ratio4x5 |
1080×1350 |
ratio3x4 |
1080×1440 |
ratio16x9 |
1920×1080 |
Sources are fitted with black bars. Sources below the frame-rate cap keep their rate. Audio: AAC stereo, 48 kHz, 256 kbps.
Supply an audio picker
await VideoEditor.present(
config: …,
pickAudio: () async => await mySoundLibrary.pick(),
);
Return a local file path (or a content:// URI on Android), or null to cancel. Without pickAudio, the system file picker opens.
Note: the callback runs while the native editor is on screen, so it can't show Flutter widgets. Pick from files already on the device or download the track on demand.
Add fonts
flutter:
fonts:
- family: Brand
fonts:
- asset: fonts/Brand-Bold.ttf
TextFontSpec(displayName: 'Brand', asset: 'fonts/Brand-Bold.ttf')
- Give the bold face. Overlay text uses one semi-bold weight.
- The asset path identifies the font in drafts. Rename it and existing text falls back to the first font.
- Files that are missing or aren't fonts are dropped from the list.
- No
UIAppFontsentry or native asset copy needed.
Resume drafts
With draftsEnabled: true, the editor saves when the user leaves: close, back, or the app going to the background. A render keeps the draft and returns its id in result.draftId.
final drafts = await VideoEditor.drafts(); // newest first
final result = await VideoEditor.present(
draftId: drafts.first.id,
config: const VideoEditorConfig(licenseKey: 've_live_…', draftsEnabled: true),
onFailure: (failure) => log('$failure'),
);
await VideoEditor.deleteDraft(drafts.first.id);
await VideoEditor.deleteAllDrafts();
- A save returns
nullfrompresent(), like a cancel. Calldrafts()again to see it. - Unchanged edits and empty timelines aren't saved. Undo history isn't saved.
draftIdrequiresdraftsEnabledand can't be combined withinitialSource.VideoEditorDraft:id,createdAt,updatedAt,durationSec,clipCount,coverPath(first-frame JPEG; empty until extracted).- Videos are referenced, not copied. If a source is gone, or iOS photo access is revoked, opening fails with
sourceUnreadableand the draft stays in the list. - Drafts are deleted only through
deleteDraftanddeleteAllDrafts, which work withdraftsEnabledoff too.
Let the user pick a cover
With coverPickerEnabled: true, "Next" opens a cover screen: the paused preview on top, a thumbnail strip of the whole video below. The user slides the frame to a time or picks a gallery photo.
- A time gives that frame of the rendered video. A photo is center-cropped to the output aspect ratio. Either way: JPEG, 480 px on the short side.
- With no selection, the cover is the first frame.
- Back returns to editing. A cancelled or failed render returns to the cover screen.
- Duration limits are checked before the cover screen. The selection isn't saved in drafts.
- Strings:
videoeditor_cover_title,videoeditor_cover_choose_from_gallery.
Show render progress in your own UI
Set detachedRenderEnabled: true to draw render progress in your own UI. The editor closes when the render starts, and the render continues as a VideoEditorRenderJob that reports progress, the result and failures.
await VideoEditor.present(
config: const VideoEditorConfig(
licenseKey: 've_live_…',
draftsEnabled: true,
detachedRenderEnabled: true,
),
onRenderStarted: (job) {
uploads.track(job);
Navigator.of(context).pushReplacementNamed('/feed');
},
);
void track(VideoEditorRenderJob job) {
job.progress.listen((value) => banner.value = value);
job.outcome.then((outcome) {
switch (outcome) {
case RenderCompleted(:final result):
upload(result.videoPath, result.coverPath);
case RenderFailed():
showRetry(job.draftId);
case RenderCancelled():
break;
}
});
}
VideoEditorRenderJob |
|
|---|---|
draftId |
Draft holding the rendered state. null when draftsEnabled is off. |
progress, latestProgress |
0 to 1. |
outcome |
Completes once with RenderCompleted(result), RenderFailed(failure) or RenderCancelled(). An empty coverPath means no cover could be extracted. |
cancel() |
Stops the render. |
onRenderStartedis called beforepresent()completes;present()then returnsnull.VideoEditor.currentRenderis the running job,nullonce it ends.- The only failure is
renderFailed, delivered throughoutcome. - One job runs at a time. Tapping "Next" in any editor meanwhile keeps the user on the editing screen and shows
videoeditor_render_busy. - To render again, open the editor with
draftId.
Read usage data
result.usage counts what's in the rendered video. Items removed during editing aren't counted.
| Field | Value |
|---|---|
videoClipCount, imageClipCount |
Clips on the timeline; each piece of a split counts |
distinctSourceCount |
Distinct source paths |
sourceDurationSec |
Total length of distinct video sources; images excluded |
adjustedVolumeClipCount, mutedClipCount |
Video clips with changed volume (muted included); muted video clips |
audioCount, adjustedAudioCount |
Audio tracks; tracks whose section within the song was moved |
stickerCount |
Stickers |
texts |
count, plus fonts, styles, colors maps (font displayName, style id, #AARRGGBB → count) |
filters, effects, transitions |
Preset id → count |
cover |
CoverUsage: source (CoverSource.frame or .photo), moved (a frame other than the first); null when the cover picker is off |
editingSec |
Time on the editing screen; excludes license check, cover screen, render, background |
renderSec |
Duration of the render that produced this result |
renderAttemptCount |
Renders started in this session, this one included; failed and cancelled count |
fullscreenOpenCount |
Fullscreen preview opens |
undoCount, redoCount |
Undo and redo actions that changed something |
Handle failures
present() throws PlatformException only when the editor can't open:
| Code | Cause |
|---|---|
editor_already_open |
Only one editor at a time |
no_activity, no_view_controller |
No screen to attach to |
bad_arguments |
Invalid configuration or output |
launch_failed |
Android: activity couldn't start |
Once open, failures go to onFailure and the editor shows its own message:
reason |
When | Then |
|---|---|---|
sourceUnreadable |
A source or draft couldn't be opened | With no clips left, the editor closes and present() returns null |
renderFailed |
Render failed | Back to editing; "Next" can be tapped again |
coverUnavailable |
Render succeeded, cover extraction failed | Result follows with an empty coverPath |
licenseInvalid |
No valid license | On open: closes, returns null. On render: stays on editing |
nativeCode is platform-specific (iOS NSError.code, Android Media3 ExportException.errorCode). Store the platform with it.
Localize
Strings are native SDK resources in English and Korean. Other languages fall back to English.
Language
The editor follows the app language. If your app has its own language setting (MaterialApp(locale:)), pass it as VideoEditorConfig.locale. On iOS, declare CFBundleLocalizations in Info.plist so the device language is picked up. A change applies the next time the editor opens.
Override strings
Define the same key in your native resources. Your value wins.
<!-- android/app/src/main/res/values/strings.xml (values-ko/, values-ja/, …) -->
<string name="videoeditor_next">Continue</string>
/* ios/Runner/en.lproj/Localizable.strings (ko.lproj, ja.lproj, …) */
"videoeditor_next" = "Continue";
- Keys start with
videoeditor_and match across platforms. Full lists:res/values/public.xml(Android SDK),Resources/en.lproj/Localizable.strings(iOS SDK). videoeditor_duration_too_shortandvideoeditor_duration_too_longare Android<plurals>and iOS.stringsdictentries.- Transition, filter and bundled font names use
videoeditor_transition_*,videoeditor_filter_*,videoeditor_font_*. Host font names are shown as given.
API
| Name | |
|---|---|
VideoEditor.present() |
Opens the editor; returns VideoEditorResult? |
VideoEditor.drafts(), deleteDraft(), deleteAllDrafts() |
Draft list and deletion |
VideoEditor.currentRender |
The running render job |
VideoEditorRenderJob, VideoEditorRenderOutcome |
Render progress and outcome for your own UI |
VideoEditorResult |
videoPath, coverPath, durationSec, audioPaths, usage, draftId |
VideoEditorUsage, TextUsage, CoverUsage, CoverSource |
Usage stats |
VideoEditorDraft |
Draft list entry |
VideoEditorFailure, VideoEditorFailureReason |
reason, nativeCode, detail |
VideoEditorConfig |
Editor configuration |
RenderOutputConfig, RenderAspectRatio, RenderVideoCodec |
Output encoding |
TextFontSpec |
Display name and pubspec asset path |
VideoEditorScreen |
Deprecated route wrapper |
Dependencies
flutter only.
License
Commercial; requires a license key from Infinishow. See LICENSE and infinive.dev.
Libraries
- flutter_video_editor_plugin
- Flutter binding for the Infinive Video Editor SDK.