# How to Add Video Editing Features to Your FlutterFlow App

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-45 min
- Compatibility: FlutterFlow Pro+ (code export required)
- Last updated: March 2026

## TL;DR

FlutterFlow does not include a built-in video editor — the FlutterFlowVideoPlayer widget only plays videos. To add editing, use the video_editor Flutter package for trim and crop via custom code, or offload heavy processing to a Firebase Cloud Function running FFmpeg on Cloud Run. Never run FFmpeg locally on files longer than 30 seconds.

## Video Editing in FlutterFlow: What Is Actually Possible

Many builders expect FlutterFlow to include a video editor alongside its video player, but the FlutterFlowVideoPlayer widget only handles playback. Actual editing — trimming clips, cropping frames, adding overlays — requires either a custom widget using the video_editor Dart package (for lightweight, on-device operations) or a server-side pipeline using Firebase Storage and a Cloud Function backed by FFmpeg running on Cloud Run (for heavy processing). This tutorial covers both paths so you can choose the right approach for your use case.

## Before you start

- FlutterFlow Pro plan with code export enabled
- Firebase project connected to your FlutterFlow app
- Firebase Storage bucket configured
- Basic understanding of FlutterFlow Custom Actions
- Cloud Functions billing enabled (Blaze plan on Firebase)

## Step-by-step guide

### 1. Understand what FlutterFlowVideoPlayer can and cannot do

Open any FlutterFlow page, drag a VideoPlayer widget from the widget panel onto the canvas, and inspect its properties. You will see controls for autoplay, looping, mute, and a video URL field — but no trim, crop, or edit options. This is by design: FlutterFlowVideoPlayer wraps the video_player Flutter package for playback only. Accept this and plan your editing pipeline outside the visual builder. For short clips under 30 seconds on modern phones, the video_editor package can do client-side trim and crop. For anything longer or more complex — resolution changes, watermarks, merging clips — route the work to a server.

**Expected result:** You understand the boundary between what FlutterFlow provides natively and what requires custom code or server processing.

### 2. Add the video_editor package as a custom dependency

In FlutterFlow, go to Settings (gear icon in the left sidebar) then click on 'Custom Code' and select 'Dependencies'. Click 'Add Dependency', enter video_editor and set the version to ^3.0.0. Also add image_picker (for selecting videos from the gallery) and path_provider (for temporary file access). Click Save after adding each dependency. These packages allow your custom widget to display a timeline scrubber, set trim points, and export the trimmed segment as a new video file — all on the user's device without any server call.

**Expected result:** Three new entries appear in your Dependencies list: video_editor, image_picker, and path_provider.

### 3. Create a Custom Widget for the video trim UI

Navigate to Custom Code in the left sidebar and click the '+' button next to 'Custom Widgets'. Name it VideoTrimmerWidget. In the code editor, write the Dart widget class that initializes a VideoEditorController with the selected file path, renders the CropGridViewer and TrimSlider from the video_editor package, and exposes an Export button that calls controller.exportVideo(). Pass the output file path back to FlutterFlow via a widget callback parameter named onExportComplete of type String. The widget should accept one parameter: videoPath of type String. Keep the trim UI minimal — a preview player, a horizontal trim slider, and an Export button is enough for most apps.

```
import 'package:flutter/material.dart';
import 'package:video_editor/video_editor.dart';
import 'dart:io';

class VideoTrimmerWidget extends StatefulWidget {
  final String videoPath;
  final void Function(String outputPath) onExportComplete;

  const VideoTrimmerWidget({
    Key? key,
    required this.videoPath,
    required this.onExportComplete,
  }) : super(key: key);

  @override
  State<VideoTrimmerWidget> createState() => _VideoTrimmerWidgetState();
}

class _VideoTrimmerWidgetState extends State<VideoTrimmerWidget> {
  late VideoEditorController _controller;
  bool _exporting = false;

  @override
  void initState() {
    super.initState();
    _controller = VideoEditorController.file(
      File(widget.videoPath),
      minDuration: const Duration(seconds: 1),
      maxDuration: const Duration(seconds: 30),
    );
    _controller.initialize().then((_) => setState(() {}));
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<void> _export() async {
    setState(() => _exporting = true);
    await _controller.exportVideo(
      onCompleted: (file) {
        setState(() => _exporting = false);
        widget.onExportComplete(file.path);
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    if (!_controller.initialized) {
      return const Center(child: CircularProgressIndicator());
    }
    return Column(
      children: [
        Expanded(
          child: CropGridViewer.preview(controller: _controller),
        ),
        TrimSlider(
          controller: _controller,
          height: 60,
        ),
        ElevatedButton(
          onPressed: _exporting ? null : _export,
          child: _exporting
              ? const CircularProgressIndicator()
              : const Text('Export Trimmed Video'),
        ),
      ],
    );
  }
}
```

**Expected result:** The custom widget appears in the FlutterFlow widget panel under Custom Widgets and can be added to any page.

### 4. Build the video selection flow on your FlutterFlow page

On your target page, add a Button widget labeled 'Select Video'. Create a new Custom Action named pickVideoAction. Inside the action, use image_picker's ImagePicker().pickVideo(source: ImageSource.gallery) to let the user choose a video, then store the returned file path in a Page State variable named selectedVideoPath (type String). Add a Conditional Widget below the button: when selectedVideoPath is not empty, show your VideoTrimmerWidget and pass selectedVideoPath as the videoPath parameter. Wire the onExportComplete callback to update a second Page State variable named exportedVideoPath. Add a second Conditional Widget that shows an upload button when exportedVideoPath is not empty.

**Expected result:** Tapping 'Select Video' opens the device gallery, selecting a video displays the trim UI, and exporting saves the trimmed file path in app state.

### 5. Upload the trimmed video to Firebase Storage

Create a Custom Action named uploadTrimmedVideo that accepts the exportedVideoPath String. Inside the action, use FirebaseStorage.instance.ref('trimmed_videos/${DateTime.now().millisecondsSinceEpoch}.mp4').putFile(File(exportedVideoPath)) to upload the trimmed file. Listen to the task.snapshotEvents stream to update a Page State variable named uploadProgress (double, 0.0-1.0) so you can display a LinearProgressIndicator. After upload completes, call task.snapshot.ref.getDownloadURL() and store the result in a Page State variable named uploadedVideoUrl. For files larger than 50 MB, consider chunked uploads by breaking the file with dart:io and reassembling with a Cloud Function.

**Expected result:** The trimmed video uploads to Firebase Storage and the download URL is stored in the page state, ready to be saved to Firestore.

### 6. Set up a Cloud Function with FFmpeg for server-side processing

For videos longer than 30 seconds or operations like resolution change, adding watermarks, or merging clips, create a Firebase Cloud Function that accepts a Firebase Storage path, processes it with FFmpeg on Cloud Run, and writes the result back to Storage. In the Firebase console, go to Functions and deploy the Node.js function below. The function reads the original file from Storage, runs FFmpeg via fluent-ffmpeg pointing at a Cloud Run container, and uploads the output. Trigger this function from FlutterFlow using a Custom Action that calls your Cloud Function's HTTPS endpoint with the storage path as the request body.

```
// functions/index.js (Node.js 18)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const { Storage } = require('@google-cloud/storage');
const os = require('os');
const path = require('path');
const fs = require('fs');
const ffmpeg = require('fluent-ffmpeg');

admin.initializeApp();
const storage = new Storage();

exports.processVideo = functions
  .runWith({ timeoutSeconds: 300, memory: '2GB' })
  .https.onCall(async (data, context) => {
    if (!context.auth) {
      throw new functions.https.HttpsError('unauthenticated', 'Login required');
    }
    const { storagePath, startTime, endTime } = data;
    const bucket = admin.storage().bucket();
    const inputTmp = path.join(os.tmpdir(), 'input.mp4');
    const outputTmp = path.join(os.tmpdir(), 'output.mp4');

    await bucket.file(storagePath).download({ destination: inputTmp });

    await new Promise((resolve, reject) => {
      ffmpeg(inputTmp)
        .setStartTime(startTime)
        .setDuration(endTime - startTime)
        .output(outputTmp)
        .on('end', resolve)
        .on('error', reject)
        .run();
    });

    const outputPath = `processed/${Date.now()}.mp4`;
    await bucket.upload(outputTmp, { destination: outputPath });

    fs.unlinkSync(inputTmp);
    fs.unlinkSync(outputTmp);

    const [url] = await bucket.file(outputPath).getSignedUrl({
      action: 'read',
      expires: Date.now() + 7 * 24 * 60 * 60 * 1000,
    });
    return { downloadUrl: url, storagePath: outputPath };
  });
```

**Expected result:** The Cloud Function appears in the Firebase console under Functions and returns a signed download URL when called with a valid storage path and time range.

## Complete code example

File: `pick_and_upload_video_action.dart`

```dart
// Custom Action: pickAndProcessVideo
// Dependencies: image_picker, firebase_storage, cloud_functions
import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'dart:io';

Future<Map<String, dynamic>> pickAndProcessVideo() async {
  // Step 1: Pick video from gallery
  final picker = ImagePicker();
  final XFile? video = await picker.pickVideo(
    source: ImageSource.gallery,
    maxDuration: const Duration(seconds: 120),
  );

  if (video == null) {
    return {'success': false, 'error': 'No video selected'};
  }

  final file = File(video.path);
  final fileSizeBytes = await file.length();

  // Step 2: Enforce 100 MB limit before upload
  if (fileSizeBytes > 100 * 1024 * 1024) {
    return {'success': false, 'error': 'File exceeds 100 MB limit'};
  }

  // Step 3: Upload original to Firebase Storage
  final storagePath = 'raw_videos/${DateTime.now().millisecondsSinceEpoch}.mp4';
  final storageRef = FirebaseStorage.instance.ref(storagePath);

  final uploadTask = storageRef.putFile(file);
  await uploadTask;

  // Step 4: Call Cloud Function for server-side trim
  // (pass start/end seconds from trim UI as parameters)
  final callable = FirebaseFunctions.instance.httpsCallable('processVideo');
  final result = await callable.call({
    'storagePath': storagePath,
    'startTime': 0,   // Replace with trim start from UI
    'endTime': 30,    // Replace with trim end from UI
  });

  final data = result.data as Map<String, dynamic>;

  // Step 5: Clean up local file
  await file.delete();

  return {
    'success': true,
    'downloadUrl': data['downloadUrl'],
    'storagePath': data['storagePath'],
  };
}
```

## Common mistakes

- **Running FFmpeg video processing on the user's device for files over 30 seconds** — FFmpeg is CPU and memory intensive. On a mid-range phone, processing a 60-second 1080p video can use over 1 GB of RAM and take several minutes, causing the OS to kill the app. Fix: Set maxDuration: Duration(seconds: 30) in VideoEditorController to gate client-side processing, and route longer videos to your Cloud Function pipeline.
- **Expecting FlutterFlow's built-in VideoPlayer to include editing controls** — The FlutterFlowVideoPlayer widget wraps the standard video_player package and only supports playback. There is no editing UI in the visual builder. Fix: Build a Custom Widget using the video_editor package for client-side trim/crop, or use a Cloud Function with FFmpeg for server-side operations.
- **Not deleting temporary video files after processing** — Each trim and export creates a new file in the device's temp directory. After several sessions the user's storage fills up, and iOS/Android may terminate the app. Fix: Call File(path).delete() after every successful upload, and also in the error handler to clean up failed attempts.
- **Calling the Cloud Function without Firebase Authentication** — The sample Cloud Function checks context.auth and throws an unauthenticated error for anonymous calls. If the user is not signed in, every call will fail silently. Fix: Ensure the user is authenticated before triggering the function. Add an auth check at the start of your Custom Action and redirect to login if needed.
- **Storing the raw upload path in Firestore before the Cloud Function finishes** — If you save the raw video URL to Firestore immediately after upload and the Cloud Function fails, users will see the unprocessed file. Fix: Only write the final URL to Firestore after the Cloud Function returns successfully. Use the processedStoragePath from the function's response.

## Best practices

- Always cap client-side video editing to 30 seconds maximum to prevent memory crashes on low-end devices.
- Display a LinearProgressIndicator wired to the Firebase Storage upload task's bytesTransferred / totalBytes to give users feedback during upload.
- Use Firebase App Check to protect your Cloud Function from unauthorized calls that could incur unexpected processing costs.
- Store both the raw and processed video paths in Firestore so you can regenerate processed versions without re-uploading the original.
- Set Firebase Storage security rules to allow writes only to users' own uid-namespaced paths: allow write: if request.auth.uid == userId.
- Add a cleanup Cloud Function scheduled daily to delete temporary raw videos older than 24 hours from the raw_videos/ bucket path.
- Test your video trim widget on both Android and iOS physical devices — emulators do not accurately represent video codec performance.

## Frequently asked questions

### Does FlutterFlow have a built-in video editor widget?

No. FlutterFlow only includes a VideoPlayer widget for playback. Any editing — trimming, cropping, adding text overlays — requires a Custom Widget using packages like video_editor, or a server-side Cloud Function with FFmpeg.

### Which Flutter package should I use for client-side video trimming?

The video_editor package (pub.dev) is the most complete option. It provides a TrimSlider, CropGridViewer, and VideoEditorController. Limit on-device processing to clips under 30 seconds to avoid memory issues.

### How do I run FFmpeg in a Firebase Cloud Function?

Install fluent-ffmpeg and @ffmpeg-installer/ffmpeg as npm dependencies in your functions directory. In your function, download the file from Storage to /tmp, run the FFmpeg command, then upload the output back to Storage. Increase memory to 2 GB in runWith options.

### Will client-side video editing work on FlutterFlow's Free plan?

The video_editor package requires code export, which is a Pro plan feature. If you are on the Free plan you cannot add custom Dart packages — you would need to upgrade to at least Pro.

### How long does the Cloud Function FFmpeg approach take?

A 60-second 1080p trim typically completes in 20-40 seconds on a 2 GB Cloud Function instance. Set the function timeout to at least 300 seconds and show a loading state in the app while waiting.

### Can I add watermarks or text overlays to videos in FlutterFlow?

Not with client-side packages easily. Text and image overlays require FFmpeg's drawtext and overlay filters, which are best handled in a Cloud Function. Send the video storage path plus overlay parameters to the function and return the processed URL.

### How should I handle the case where the Cloud Function times out?

Wrap your Custom Action call in a try-catch and check for the deadline-exceeded error code. Show the user a message explaining the video was too large or complex, and offer to retry with a shorter segment.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-use-flutterflow-s-built-in-video-editing-features
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-use-flutterflow-s-built-in-video-editing-features
