# How to Add Video Clipping and Editing to Your App — Copy-Paste Prompts Inside

- Tool: App Features
- Last updated: July 2026

## TL;DR

Video clipping on mobile needs two things: a timeline UI with draggable in/out handles and an on-device FFmpeg engine (ffmpeg_kit_flutter) to perform the trim. All processing runs on the user's device — $0 compute cost. With FlutterFlow you can wire the timeline UI and video_player in 3–6 days, but the FFmpeg command string requires a Custom Action with hand-written Dart. Export and gallery save work on both iOS and Android with the right permission setup.

## What a Video Clipping and Editing Tool Actually Is

A video editing feature lets users trim raw recordings down to the moment that matters, add text captions, and export a polished clip to their gallery or cloud — all without leaving the app. The underlying engine is FFmpeg, the open-source media processing library, running on-device via the ffmpeg_kit_flutter package. Because processing is local, there are no per-minute cloud costs and no video ever leaves the device unless the user explicitly saves to cloud. The product decisions that determine build complexity are: whether you need frame-accurate trimming vs. stream-copy (much faster but limited to keyframe boundaries), whether text overlays are static or animated, and whether the output goes to the device gallery, your cloud storage, or a sharing API.

## Anatomy of the Feature

Six components. FlutterFlow handles the video player and scrubber UI well visually; the FFmpeg command string and export pipeline must be written in a Custom Action. That Custom Action is where every broken first build fails.

- **Media picker** (ui): image_picker (^1.1.2) for selecting video from the device gallery. permission_handler package manages the Photos/MediaLibrary permission request on both iOS and Android. file_picker provides a document picker fallback on iOS when image_picker cannot access certain library locations.
- **Video player and scrubber** (ui): video_player Flutter package (^2.9.0) for playback. A custom Flutter widget wraps a RangeSlider mapped to start_time and end_time in seconds. The player's seekTo() is called on range change to keep playback in sync with the selected range. Loop playback is achieved by listening to the position stream and seekTo(startTime) when position exceeds endTime.
- **Timeline thumbnail strip** (ui): VideoThumbnail package extracts JPEG frames at configurable intervals (e.g. one frame every 500 ms of video). Thumbnails are displayed in a horizontal ListView with fixed-width cells aligned to the RangeSlider below. Each thumbnail gives the user a visual reference for what content is in each section of the video.
- **Text overlay layer** (ui): Flutter Stack widget with Positioned draggable Text widgets. An 'Add Text' button opens a bottom sheet with a TextField, font size slider (from the google_fonts package), and color picker (flutter_colorpicker). Each text overlay stores its text, position (dx, dy as fractions of video dimensions), font size, and color — passed to FFmpeg as drawtext filter parameters during export.
- **Frame extraction engine** (backend): ffmpeg_kit_flutter (^6.0.3) runs FFmpeg commands within the Flutter app sandbox. For stream-copy trim (fastest, no re-encode): FFmpegKit.executeAsync('-ss {start} -to {end} -i {input} -c copy {output}'). For text overlays or re-encoding to constrain file size: add -vf drawtext=... and -crf 28 -vf scale=1280:-2 flags.
- **Export pipeline** (backend): After FFmpeg completes, image_gallery_saver copies the output file from the app's temporary directory to the device's photo library. The export result callback from FFmpegKit provides the return code (RC); RC 0 means success. A SnackBar confirms success or failure. Optionally, the exported file is uploaded to Supabase Storage for cloud backup.

## Data model

One table stores video project metadata so users can resume unfinished edits. Run this in the Supabase SQL editor:

```sql
create table public.video_projects (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  original_asset_url text,
  output_asset_url text,
  clips jsonb not null default '[]',
  text_overlays jsonb not null default '[]',
  status text not null default 'draft'
    check (status in ('draft', 'exporting', 'exported', 'error')),
  created_at timestamptz not null default now()
);

alter table public.video_projects enable row level security;

create policy "Users can manage own video projects"
  on public.video_projects for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

create index video_projects_user_recent_idx
  on public.video_projects (user_id, created_at desc);
```

The clips jsonb column stores [{start_time, end_time}] for multi-clip projects. text_overlays stores [{text, dx, dy, font_size, color}]. The status enum enables a resume-later flow where users return to find their draft edits intact.

## Build paths

### Flutterflow — fit 3/10, 3–6 days

FlutterFlow handles image_picker, video_player, and the timeline slider UI visually. The FFmpeg trim command requires a Custom Action block with hand-written Dart, but that Custom Action is the only code you write — the rest is visual configuration.

1. Add image_picker, video_player, ffmpeg_kit_flutter, VideoThumbnail, path_provider, and image_gallery_saver to the project's pubspec dependencies in FlutterFlow Settings → Pub Dependencies
2. Build the video picker screen: add a Button with an Action that calls image_picker to select a video; store the file path in App State
3. In the Action editor, add a Custom Action for thumbnail extraction: call VideoThumbnail.thumbnailsInRange() with the video path and a 500 ms interval; store the resulting bytes list in App State
4. Build the editor screen: add a video_player widget bound to the video path; add a RangeSlider widget mapping values to startTime and endTime App State variables; add a horizontal ListView displaying thumbnail bytes; add a Stack with Positioned draggable Text widgets for overlays
5. Add a Custom Action for the FFmpeg export: use getTemporaryDirectory() to build the output path, construct the FFmpeg command string with start/end times and optional drawtext filters, call FFmpegKit.executeAsync(), await the return code, then call image_gallery_saver; show a CircularProgressIndicator with cancel button during export using a loading state

Limitations:

- ffmpeg_kit_flutter cannot be wired visually in FlutterFlow — the Custom Action Dart code must be written by hand, which requires Dart knowledge
- FlutterFlow generates hardcoded file path strings; the Custom Action must use getTemporaryDirectory() from path_provider to build platform-correct paths dynamically
- The FlutterFlow RangeSlider widget does not natively seek the video player on drag — a custom Dart callback is needed to connect them

### Lovable — fit 1/10, Not recommended

Lovable generates Vite/React web apps, not native iOS/Android apps. Browser-based video editing with ffmpeg.wasm is possible but produces files 10x heavier and 5x slower than native on-device FFmpeg. This is the wrong platform for mobile video editing.

1. Skip Lovable for this feature — it cannot produce a native mobile app
2. If a web-based video trimmer is acceptable, consider ffmpeg.wasm in a Next.js app (custom dev path)
3. For native mobile, use the FlutterFlow path instead

Starter prompt:

```
Build a web video trimmer using ffmpeg.wasm. Import @ffmpeg/ffmpeg and @ffmpeg/util (install via npm). On mount, call ffmpeg.load() with the core wasm files from a CDN (unpkg.com/@ffmpeg/core); show a loading indicator until ready. File input: accept video/* files up to 500 MB; display the selected video in an HTML5 video element. Trim UI: two range inputs for start_time and end_time in seconds, initialized to 0 and video.duration; display current values as MM:SS. Thumbnail strip: extract one frame every 5 seconds using ffmpeg.exec(['-i', 'input.mp4', '-vf', 'fps=1/5,scale=80:-1', 'thumb_%03d.jpg']) and display as a horizontal filmstrip. Trim button: call ffmpeg.exec(['-i', 'input', '-ss', startTime, '-to', endTime, '-c', 'copy', 'output.mp4']) with a CircularProgress showing percentage from ffmpeg.on('progress'). On completion, create a download link with URL.createObjectURL for the output blob. Add a cancel button that calls ffmpeg.terminate() and reloads the wasm worker. Handle states: idle, loading-wasm, file-selected, trimming with progress percentage, done with download link, error with retry.
```

Limitations:

- Lovable produces a Vite/React web app, not a native iOS/Android app
- ffmpeg.wasm in-browser is extremely heavy (~30 MB download) and significantly slower than native on-device FFmpeg
- Native features like image_gallery_saver and permission_handler are not available in a web app

### Custom — fit 5/10, 4–8 weeks

Full native Flutter with ffmpeg_kit_flutter gives frame-accurate edits, hardware-accelerated export on supported devices, full codec control, and background export via WorkManager on Android. Choose this path when video quality or export speed is a product differentiator.

1. Native Flutter project with ffmpeg_kit_flutter, custom timeline widget using RenderObject for pixel-accurate scrubbing, and thumbnail extraction in compute isolates
2. Hardware-accelerated encode using h264_videotoolbox on iOS and h264_mediacodec on Android for 3–5x faster export
3. Background export with WorkManager (Android) and BGTaskScheduler (iOS) so users can close the app during long exports
4. Multi-clip merge using FFmpeg concat filter with cross-dissolve transitions
5. Integration with proprietary video CDN (Mux, Cloudflare Stream) for streaming the exported clip

Limitations:

- 4–8 week timeline requires engineers with FFmpeg command expertise and native iOS/Android deployment experience
- Hardware codec acceleration varies by device — test on both flagship and mid-range Android devices

## Gotchas

- **Export silently succeeds but no file appears in iOS gallery** — On iOS, image_gallery_saver requires the NSPhotoLibraryAddUsageDescription permission key in Info.plist. Without it, the export callback from ffmpeg_kit_flutter returns RC 0 (success) — FFmpeg completed without errors — but image_gallery_saver cannot write to the gallery and silently discards the file. The user sees a 'Saved!' SnackBar but finds nothing in Photos. Fix: Add NSPhotoLibraryAddUsageDescription to your app's Info.plist with a meaningful description ('Used to save your edited videos to Photos'). In FlutterFlow, add this under Project Settings → iOS Info.plist. Also call Permission.photos.request() via permission_handler before invoking image_gallery_saver and check the result — show an error if permission is denied.
- **Video player shows black frame on first load** — Initializing video_player without awaiting controller.initialize() causes the first call to play() to fail silently: the video element renders black for 1–3 seconds, and if the user taps play again quickly, a duplicate controller is created. On slower Android devices this compounds into an audio-video sync issue that persists for the session. Fix: Call controller = VideoPlayerController.file(file) in initState(), immediately follow with await controller.initialize(), and only then call setState() to render the video widget and call controller.play(). Display a shimmer loading placeholder bound to a _isInitialized boolean while the await resolves.
- **RangeSlider seek causes stuttering during drag** — Calling videoPlayerController.seekTo(Duration(seconds: value)) inside onChanged fires on every pointer event during drag — potentially hundreds of calls per second. The VideoPlayerController cannot handle rapid seek requests and stalls, causing visible stuttering and making the scrub experience feel broken. Fix: Debounce seek calls with a 100 ms Timer: cancel any pending timer on each onChanged event, start a new 100 ms timer that performs the seek. Only call seekTo on onChangeEnd for the actual playback position update. This keeps the RangeSlider position responsive while limiting seek calls to one per drag gesture.
- **FlutterFlow Custom Action uses hardcoded file paths** — FlutterFlow's code generation creates Custom Actions with hardcoded path strings like '/var/mobile/Containers/...' that were valid on the development device but break on any other device or OS version update. The FFmpeg command fails with 'No such file or directory' because the input file path no longer exists. Fix: Inside the Custom Action, always call getTemporaryDirectory() from path_provider to get the platform-correct temporary directory path, then construct the absolute file path dynamically: final dir = await getTemporaryDirectory(); final outputPath = '${dir.path}/clip_${DateTime.now().millisecondsSinceEpoch}.mp4';. Never hardcode any path segment.
- **Export produces multi-GB files from 4K source videos** — ffmpeg_kit_flutter's stream-copy trim (-c copy flag) copies the original codec without re-encoding — perfect for speed, but it preserves the original bitrate. A 30-second trim from a 4K iPhone video can produce a 400–800 MB output file, making gallery save very slow and cloud upload impractical. Fix: Add re-encoding flags to the FFmpeg command: -vf scale=1280:-2 -c:v libx264 -crf 28 -c:a aac. The scale filter resizes to 1280px wide while preserving aspect ratio; -crf 28 produces a visually acceptable quality at ~5–8 Mbps for 1080p, reducing file sizes by 5–10x. Warn users that re-encoding takes longer than stream copy.

## Best practices

- Always construct FFmpeg output paths using getTemporaryDirectory() from path_provider — hardcoded paths break across devices and OS updates
- Debounce videoPlayerController.seekTo() calls during RangeSlider drag to 100 ms — raw onChanged calls cause seek-queue overflow and stuttering
- Show a CircularProgressIndicator with a cancel button during export — FFmpeg on mid-range Android can take 30+ seconds for a 60-second clip with re-encoding
- Add NSPhotoLibraryAddUsageDescription to Info.plist from day one — missing it is the single most common silent failure in this feature on iOS
- Extract thumbnails in a compute isolate or show a shimmer strip immediately — synchronous thumbnail extraction blocks the UI thread for 2–5 seconds on long videos
- Constrain export size with -vf scale=1280:-2 -crf 28 by default — users don't need 4K exports from a phone editor and large files slow everything downstream
- Clean up temporary files in the app's temp directory after export or upload — ffmpeg output files accumulate quickly and consume device storage

## Frequently asked questions

### Can I trim a video without re-encoding it on mobile?

Yes — stream-copy trim with the -c copy FFmpeg flag performs a near-instant cut along keyframe boundaries without re-encoding. The trade-off: the cut is not truly frame-accurate because it snaps to the nearest keyframe, which may be up to a second from where you set the handle. For social clips where a 1-second rounding is acceptable, stream-copy is significantly faster. For precise cuts, add re-encoding flags (-c:v libx264 -crf 28) which process every frame.

### What's the maximum video size ffmpeg_kit_flutter can handle?

There is no hard limit imposed by the library — the constraint is available device RAM and storage. ffmpeg_kit_flutter streams frames through memory rather than loading the entire file at once, so even multi-GB 4K videos process successfully. However, re-encoding a 60-minute 4K video can take 20+ minutes on a mid-range phone. Practically, test with the upper bound of what your users would plausibly record — usually 5–15 minutes of 1080p.

### How do I add captions or subtitles to a clipped video in Flutter?

Use the FFmpeg drawtext filter: add a -vf "drawtext=text='Caption text':fontsize=48:fontcolor=white:x=(w-text_w)/2:y=h-100" flag to the FFmpeg command string. For multiple captions at different timestamps, stack multiple drawtext filters with enable='between(t,start,end)' timing expressions. This burns the text into the video. For overlay-only captions (not burned in), keep the text as a separate Flutter widget layer and only burn during final export.

### Can FlutterFlow handle video editing or do I need custom code?

FlutterFlow handles the UI layer — video player, scrubber, thumbnail strip, text overlay positioning — visually. The FFmpeg command execution requires a Custom Action with hand-written Dart. It's roughly a 50/50 split: half is visual configuration, half is a single Custom Action that contains the FFmpeg logic. If you're comfortable writing 30–40 lines of Dart in a Custom Action block, FlutterFlow is a solid foundation for this feature.

### How do I save an edited video to the device gallery on iOS and Android?

Use the image_gallery_saver package after FFmpeg completes. On iOS, add NSPhotoLibraryAddUsageDescription to Info.plist and request Permission.photos via permission_handler before saving. On Android, no additional permissions are needed for API level 29 and above (scoped storage). Call GallerySaver.saveVideo(outputPath) and check the result boolean — show a SnackBar on success and an error dialog on failure.

### What permissions does video editing require on iOS vs Android?

On iOS: NSPhotoLibraryUsageDescription (to read videos from Photos for import) and NSPhotoLibraryAddUsageDescription (to save exports back to Photos). Both are required in Info.plist or the App Store will reject the build. On Android 13+: READ_MEDIA_VIDEO is required to read from the gallery; WRITE_EXTERNAL_STORAGE is not needed on API 29+ due to scoped storage. Add both iOS descriptions in FlutterFlow under Project Settings → iOS Info.plist Entries.

### How long does it take to export a 1-minute trimmed clip?

Stream-copy trim (-c copy): nearly instant, 1–3 seconds regardless of clip length. Re-encoding to 1080p with -crf 28: 30–90 seconds on a mid-range Android (Snapdragon 680-class) and 15–30 seconds on an iPhone 12 or newer. Enable hardware acceleration if available — h264_videotoolbox on iOS and h264_mediacodec on Android reduce re-encode times by 3–5x. Always show a progress indicator with estimated time remaining during re-encode exports.

### Can I merge two video clips together in a Flutter app?

Yes, using ffmpeg_kit_flutter's concat filter. Create a text file listing the input paths (the FFmpeg concat demuxer format), then run: FFmpegKit.executeAsync('-f concat -safe 0 -i {list_file} -c copy {output}'). For transitions between clips, add -filter_complex with xfade expressions, which requires re-encoding. Write the list file to getTemporaryDirectory() using Dart's File API, then reference that path in the FFmpeg command.

---

Source: https://www.rapidevelopers.com/app-features/video-clipping-and-editing-tool
© RapidDev — https://www.rapidevelopers.com/app-features/video-clipping-and-editing-tool
