flutter_video_editor_plugin 1.0.8
flutter_video_editor_plugin: ^1.0.8 copied to clipboard
Timeline-based multi-clip video editor: trim, split, stickers, text, color filters, transitions and audio. Preview and export share one native composition.
flutter_video_editor_plugin #
Flutter binding for the Infinive video editor SDK: a timeline-based, multi-clip video editor with trim, split, stickers, text, color filters, transitions and background audio, exporting 9:16 MP4.
The editing screen is native. On iOS VideoEditorCore and on Android the show.infini:videoeditor SDK provide the screen, the engine and the result. This package presents that screen and returns the result. Nothing crosses the platform channel per frame.
No third-party media SDK is involved. Source selection, metadata, thumbnails, waveforms, preview composition and rendering are handled by the native engines (Media3 on Android; AVFoundation and Core Image on iOS). The preview and the export are rendered from the same composition, so what is shown on screen is what is exported.
A license key is required to open the editor. See Licensing and infinive.dev. Full documentation: infinive.dev/docs.
Features #
- Video selection from the gallery, with multiple clips; photos become image clips
- Timeline editing with pinch zoom and scrubbing
- Trim, split, delete and per-clip volume
- Stickers (GIPHY stickers with transparent backgrounds), text, color filters, video effects, clip transitions and background audio
- Undo and redo, up to 50 steps
- Drafts: leaving the editor saves the editing state, and the host can resume it later
- 9:16 MP4 export with a cover image
Installation #
dependencies:
flutter_video_editor_plugin: ^1.0.8
The plugin depends on the native SDKs by name: the VideoEditorCore pod on iOS and the show.infini:videoeditor Maven coordinate on Android. The published artifacts are fetched automatically; the plugin adds the Maven repository to the Android build.
Android #
minSdk24 or later.- The editor must be launched from a
FlutterActivity. The plugin attaches asActivityAwareto receive the editor’s result. - No additional permissions. Video selection uses the photo picker, and the
INTERNETpermission used for GIPHY is declared in the SDK manifest.
iOS #
- Podfile:
platform :ios, '16.0'. Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo access is needed to pick videos to edit.</string>
The iOS editor requests photo-library read access immediately before opening the picker, so that it can use the original file without copying it. Only full access is accepted. If access is denied or limited to selected photos, the editor shows an explanation with a button that opens Settings, and returns null if no clips are present. Passing a file through initialSource does not involve this permission. On Android the photo picker provides URIs without a permission, so no request is made.
No camera or microphone keys are required. Audio is picked from the Files app through the document picker.
Usage #
import 'package:flutter_video_editor_plugin/flutter_video_editor_plugin.dart';
final result = await VideoEditor.present(
// When omitted, the editor opens the gallery (videos and photos; a photo becomes a one-second image clip).
// When provided, the editor starts with that video or photo, for example after capture or from the host’s own album screen.
// On Android, a photo given as a file path must have its extension.
initialSource: pickedPath,
// licenseKey is required; the license server must have this application (iOS bundle ID and Android package name) bound to the key. See “Licensing”.
config: const VideoEditorConfig(licenseKey: 've_live_…', giphyApiKey: '…'),
// Render failures and similar conditions. The screen remains open and the editor informs the user. Record the failure; see “Failures”.
onFailure: (failure) => log('editor failed: $failure'),
);
if (result != null) {
print(result.videoPath); // the rendered MP4
print(result.coverPath); // the cover JPEG; an empty string means no cover is available
print(result.durationSec);
print(result.audioPaths); // audio tracks used, in timeline order, as your pickAudio callback supplied them
}
The native screen is presented over the current screen. It is not a Flutter route and does not involve Navigator. The result is returned as a future, and null means the user left the editor; it is not a failure.
Both paths refer to temporary files in the application cache. The editor does not save anywhere else; storing the files in the gallery, uploading them or deleting them is the host’s responsibility. The cover is JPEG on both platforms.
The VideoEditorScreen widget is deprecated. It remains as a wrapper so that hosts that pushed it as a route continue to work, and it will be removed in the next major version.
Configuration #
VideoEditor.present(
config: VideoEditorConfig(
// Required. See “Licensing”.
licenseKey: 've_live_…',
// Without it, the sticker button is shown disabled.
giphyApiKey: myGiphyKey,
// Text font presets. null selects the three bundled fonts.
// Each entry is a font file declared in the pubspec, by its asset path. Give the bold face:
// overlay text is rendered in one semi-bold weight. Nothing is registered natively. See “Fonts”.
textFonts: const [
TextFontSpec(displayName: 'Brand', asset: 'fonts/Brand-Bold.ttf'),
],
// Exportable length in seconds. null means no limit.
// Editing is not restricted. When “Next” is tapped outside the range, the editor informs the user and does not render.
minDurationSeconds: 3,
maxDurationSeconds: 60,
// false hides the add button on the timeline and prevents deleting the last clip.
allowAddClip: true,
// Language of the editor’s strings. Set this only if the application has its own language setting. See “Localization”.
locale: Localizations.localeOf(context).toLanguageTag(),
// Save the editing state on the device without rendering. See “Drafts”.
draftsEnabled: true,
// Show a cover-selection screen between “Next” and the render. See “Cover picker”.
coverPickerEnabled: true,
),
// Output encoding. Only the fields that are set are overridden; the remaining defaults are
// 1080×1920, a 7 Mbps cap, a 30 fps cap and H.264.
output: const RenderOutputConfig(bitrate: 5000000),
);
Audio picker #
The editor’s audio picker is the system file picker. When pickAudio is provided, it is called first whenever the user adds or replaces audio, and the returned local file path (or content:// URI on Android) is imported. Returning null cancels; nothing happens. The system picker is used only when pickAudio is not provided.
await VideoEditor.present(
config: …,
pickAudio: () async => await mySoundLibrary.pick(), // a local file path, or null
);
The callback runs while the native editor is in front, so Flutter widgets cannot be shown. The callback must produce a path without a Flutter screen, for example from files already on the device or a track downloaded on demand. Native hosts use VideoEditorViewController.pickAudio on iOS and VideoEditorActivity.intent(audioPicker=) on Android, and those can present their own screen over the editor.
Licensing #
The licenseKey identifies the application. The editor is opened by a token that the license server (license.infinive.dev) signs after checking the key, the application binding (iOS bundle ID and Android package name) and the contract period. The native SDK caches the token on the device for seven days and does not contact the server while it is valid, so the editor works offline for that period. A new token is fetched the first time the editor opens after expiry; if the device is offline at that moment, the editor does not open. The first launch must be online.
- Keys are issued by Infinishow. See infinive.dev for contact.
- Register the iOS bundle ID and the Android package name. Build variants such as
.qaor.stagingare registered separately. On a mismatch,onFailurereceiveslicenseInvalidwithdetailset toapp_mismatch: android <package name>. Send that line to have the application bound. - Test keys (
ve_test_) receive tokens only from debug builds. A test key in a release build results intest_key_in_release. - An empty key results in
bad_arguments. The full rules are in the licensing specification.
Fonts #
Declare the font file in the pubspec as usual and pass its asset path:
flutter:
fonts:
- family: Brand
fonts:
- asset: fonts/Brand-Bold.ttf
TextFontSpec(displayName: 'Brand', asset: 'fonts/Brand-Bold.ttf')
The plugin locates the file inside the application bundle through Flutter’s asset lookup on both platforms. On iOS it also registers the file with Core Text, which Flutter itself does not do, so no UIAppFonts entry is needed. On Android the SDK reads the file through AssetManager, so nothing is copied into the native assets/ directory.
Give the bold face. Overlay text is rendered in a single semi-bold weight, and synthetic bold is not applied. A file that is missing or is not a font is omitted from the list; no system font is substituted. The asset path is also the font’s identity in saved drafts: if it changes later, text that used it is rendered in the first font of the list.
Drafts #
With draftsEnabled: true, leaving the editing screen saves the editing state: closing the editor, navigating back or moving the application to the background saves without prompting, and a completed render deletes the draft. Nothing is written when nothing has changed or when there are no clips; opening a video and leaving without editing does not create a draft. The undo history is not saved.
// List, most recently saved first. The list screen is the host’s.
final drafts = await VideoEditor.drafts();
for (final d in drafts) {
print('${d.id} ${d.clipCount} clips ${d.durationSec}s ${d.updatedAt} ${d.coverPath}');
}
// Resume. draftsEnabled must be true, and draftId cannot be combined with initialSource.
final result = await VideoEditor.present(
draftId: drafts.first.id,
config: const VideoEditorConfig(licenseKey: 've_live_…', draftsEnabled: true),
onFailure: (failure) => log('$failure'), // if the draft cannot be opened: sourceUnreadable, then null
);
// Delete. This is the only way to discard a draft.
await VideoEditor.deleteDraft(drafts.first.id);
- A save is returned from
present()asnull, the same as a cancellation. The host learns of the draft by callingdrafts()again. coverPathis a first-frame JPEG and is empty when no cover exists (immediately after a save it may not have been extracted yet).- If a draft cannot be opened (a source is missing or a file is corrupt),
onFailurereceivessourceUnreadableand the editor closes. The draft remains in the list; deleting it is the host’s decision. - Videos are referenced, not copied: files passed through
initialSource, Android gallery URIs (with a persistable read permission), and iOS photo-library identifiers. If the original is deleted, or the permission is revoked on iOS, the draft cannot be opened. A draft contains only the cover, stickers and, on iOS, audio copies. - Listing and deleting work regardless of
draftsEnabled.
Cover picker #
With coverPickerEnabled: true, a cover screen follows “Next”. The upper part is the editing screen’s preview (the same composition, with filters, text, stickers and transitions) paused at the chosen time; below it is a strip of thumbnails from the start to the end of the video. Slide the white frame to choose a time, or tap “Choose from gallery” to pick a photo. The duration limit is checked at the first “Next”; when the length is out of range, the cover screen is not shown.
- A chosen time yields that frame of the rendered video; a photo is center-cropped to the output aspect ratio. Size and format are unchanged (JPEG, 480 px on the short side). With no selection, the cover is the first frame, as when the option is off.
- Back returns to the editing screen. Cancelling or failing a render returns to the cover screen with the selection kept.
- The selection is not saved in drafts. The shapes of the result and of failures are unchanged.
- Two additional strings:
videoeditor_cover_titleandvideoeditor_cover_choose_from_gallery.
Failures #
Only failures to present the screen are thrown as a PlatformException.
| Code | Meaning |
|---|---|
editor_already_open |
The editor is already open. Only one instance is allowed. |
no_activity, no_view_controller |
There is no screen to attach to. |
bad_arguments |
The configuration or output did not pass the SDK’s validation (an empty licenseKey, a minimum greater than the maximum, or an unknown codec). |
launch_failed |
Android: the activity could not be started. |
Failures after the editor is open are delivered through onFailure; the future is unaffected. The screen remains open and the editor informs the user. The host only records the failure. User actions are not reported here.
reason |
When | Afterwards |
|---|---|---|
sourceUnreadable |
A picked source or a draft could not be opened | If no clips remain, the editor closes and the future completes with null |
renderFailed |
The render failed (nativeCode is the platform’s original code) |
The editing screen returns and “Next” can be tapped again |
coverUnavailable |
The render succeeded but no cover could be extracted | The result follows with an empty coverPath |
licenseInvalid |
No valid license: unknown key, application not bound, contract ended, or first launch while offline (see “Licensing”) | On open: the editor closes and the future completes with null. At render start: the editing screen remains |
The meaning of nativeCode differs per platform (iOS NSError.code; Android Media3 ExportException.errorCode). Record the platform with it when aggregating.
Localization #
The editor’s strings are resources of the native SDKs. Korean and English are bundled, and English is used when the application language is neither. There is no strings class in Dart, and strings are not passed over the channel.
Language. The default is the application language as seen by the operating system. Applications with an in-app language setting (MaterialApp(locale:)) pass it through VideoEditorConfig.locale, typically as Localizations.localeOf(context).toLanguageTag(). For iOS to follow the device language, the application’s Info.plist must declare CFBundleLocalizations, as described in the iOS section of Flutter’s internationalization guide.
Changing a string or adding a language. Define the same key in the application’s native resources, in the same place permission strings are localized. The application’s value takes precedence.
<!-- 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 are identical on both platforms and begin with
videoeditor_. The lists are in the SDKs’res/values/public.xmlandResources/en.lproj/Localizable.strings. - The two strings that include numbers (
videoeditor_duration_too_shortandvideoeditor_duration_too_long) are Android<plurals>and iOS.stringsdictentries. - The names of transitions, filters and bundled fonts are in the same table (
videoeditor_transition_*,videoeditor_filter_*,videoeditor_font_*). ThedisplayNameof a host-supplied font is shown as given. - A change of
localetakes effect the next time the editor is opened.
Public API #
| Name | Description |
|---|---|
VideoEditor.present() |
Presents the editor and returns the result. Failures are delivered through onFailure; draftId resumes a draft; pickAudio supplies a host audio picker. |
VideoEditor.drafts(), VideoEditor.deleteDraft() |
List and delete drafts. |
VideoEditorResult |
The result: videoPath, coverPath, durationSec, audioPaths. |
VideoEditorDraft |
A draft entry: id, createdAt, updatedAt, durationSec, clipCount, coverPath. |
VideoEditorFailure, VideoEditorFailureReason |
Why the editor produced no result: reason, nativeCode, detail. |
VideoEditorConfig |
Configuration: license key (required), GIPHY key, text fonts, duration limits, add-clip control, locale, drafts and the cover picker. |
RenderOutputConfig, RenderVideoCodec |
Output resolution, bitrate, frame-rate cap and codec. |
TextFontSpec |
A text font preset with per-platform fields. |
VideoEditorScreen |
Deprecated widget wrapper around present(). |
Dependencies #
flutter only. Everything else is contained in the native SDKs.
License #
Commercial. Use requires a license key issued by Infinishow. See the LICENSE file and infinive.dev.