Skip to main content

flutter_video_editor_plugin 1.1.4 copy "flutter_video_editor_plugin: ^1.1.4" to clipboard
flutter_video_editor_plugin: ^1.1.4 copied to clipboard

Native timeline video editor for Flutter apps on iOS and Android. Presents the editor and returns the rendered MP4.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_video_editor_plugin/flutter_video_editor_plugin.dart';
import 'package:gal/gal.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(theme: ThemeData.dark(), home: const HomePage());
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

enum _SaveState { idle, saving, saved, failed }

class _HomePageState extends State<HomePage> {
  VideoEditorResult? _lastResult;
  _SaveState _saved = _SaveState.idle;
  String? _saveError;

  bool _draftsEnabled = false;
  bool _coverPickerEnabled = true;
  RenderAspectRatio _aspectRatio = RenderAspectRatio.ratio9x16;
  int _bitrate = 7_000_000;
  int _maxFrameRate = 30;
  RenderVideoCodec _codec = RenderVideoCodec.h264;
  List<VideoEditorDraft> _drafts = const [];

  // Demo keys. Do not ship these.
  static const _giphyApiKey = 'RLm06zs0imxFCeWYaRLILoFEoMNUSquN';
  static const _licenseKey = 've_live_ge7y5gywjua57qj99sk6gaugs87c599g';

  VideoEditorConfig get _config => VideoEditorConfig(
    licenseKey: _licenseKey,
    giphyApiKey: _giphyApiKey,
    minDurationSeconds: 3,
    maxDurationSeconds: 120,
    draftsEnabled: _draftsEnabled,
    coverPickerEnabled: _coverPickerEnabled,
  );

  RenderOutputConfig get _output => RenderOutputConfig(
    aspectRatio: _aspectRatio,
    bitrate: _bitrate,
    maxFrameRate: _maxFrameRate,
    codec: _codec,
  );

  @override
  void initState() {
    super.initState();
    _reloadDrafts();
  }

  Future<void> _reloadDrafts() async {
    final drafts = await VideoEditor.drafts();
    if (mounted) setState(() => _drafts = drafts);
  }

  Future<void> _openEditor({String? initialSource, String? draftId}) async {
    final result = await VideoEditor.present(
      initialSource: initialSource,
      draftId: draftId,
      config: _config,
      output: _output,
      onFailure: (failure) => debugPrint('[example] editor failed: $failure'),
    );
    if (!mounted) return;
    await _reloadDrafts();
    if (result == null || !mounted) return;
    setState(() {
      _lastResult = result;
      _saved = _SaveState.saving;
      _saveError = null;
    });
    await _saveToGallery(result);
  }

  Future<void> _saveToGallery(VideoEditorResult result) async {
    try {
      if (!await Gal.hasAccess() && !await Gal.requestAccess()) {
        _saveFailed('access denied');
        return;
      }
      await Gal.putVideo(result.videoPath);
      if (mounted) setState(() => _saved = _SaveState.saved);
    } on GalException catch (e) {
      debugPrint('[example] save failed: $e');
      _saveFailed(e.type.name);
    }
  }

  void _saveFailed(String why) {
    if (!mounted) return;
    setState(() {
      _saved = _SaveState.failed;
      _saveError = why;
    });
  }

  Future<void> _openEditorWithPicked() async {
    final picked = await ImagePicker().pickVideo(source: ImageSource.gallery);
    if (picked == null || !mounted) return;
    await _openEditor(initialSource: picked.path);
  }

  Future<void> _openDraft(VideoEditorDraft draft) async {
    setState(() => _draftsEnabled = true);
    await _openEditor(draftId: draft.id);
  }

  Future<void> _deleteDraft(VideoEditorDraft draft) async {
    await VideoEditor.deleteDraft(draft.id);
    await _reloadDrafts();
  }

  String get _saveLabel => switch (_saved) {
    _SaveState.idle => '',
    _SaveState.saving => 'Saving to gallery…',
    _SaveState.saved => 'Saved to gallery',
    _SaveState.failed => 'Could not save to gallery${_saveError == null ? '' : ' — $_saveError'}',
  };

  @override
  Widget build(BuildContext context) {
    final result = _lastResult;
    return Scaffold(
      appBar: AppBar(title: const Text('Video Editor Demo')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 24),
        children: [
          Center(
            child: Column(
              children: [
                ElevatedButton(
                  onPressed: _openEditor,
                  child: const Text('Open Video Editor'),
                ),
                const SizedBox(height: 12),
                OutlinedButton(
                  onPressed: _openEditorWithPicked,
                  child: const Text('Pick first, then edit'),
                ),
              ],
            ),
          ),
          SwitchListTile(
            title: const Text('Drafts'),
            subtitle: const Text('Leaving the editor saves a draft'),
            value: _draftsEnabled,
            onChanged: (value) => setState(() => _draftsEnabled = value),
          ),
          SwitchListTile(
            title: const Text('Cover picker'),
            subtitle: const Text('Choose a cover frame or photo before rendering'),
            value: _coverPickerEnabled,
            onChanged: (value) => setState(() => _coverPickerEnabled = value),
          ),
          _ChoiceTile<RenderAspectRatio>(
            title: 'Aspect ratio',
            value: _aspectRatio,
            options: {for (final ratio in RenderAspectRatio.values) ratio: ratio.wireName},
            onChanged: (value) => setState(() => _aspectRatio = value),
          ),
          _ChoiceTile<int>(
            title: 'Bitrate cap',
            value: _bitrate,
            options: {
              for (final mbps in const [3, 5, 7, 10, 15]) mbps * 1_000_000: '$mbps Mbps',
            },
            onChanged: (value) => setState(() => _bitrate = value),
          ),
          _ChoiceTile<int>(
            title: 'Frame rate cap',
            value: _maxFrameRate,
            options: {for (final fps in const [24, 30, 60]) fps: '$fps fps'},
            onChanged: (value) => setState(() => _maxFrameRate = value),
          ),
          _ChoiceTile<RenderVideoCodec>(
            title: 'Codec',
            value: _codec,
            options: const {RenderVideoCodec.h264: 'H.264', RenderVideoCodec.hevc: 'HEVC'},
            onChanged: (value) => setState(() => _codec = value),
          ),
          if (result != null) _ResultSection(result: result, saveLabel: _saveLabel, saved: _saved),
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
            child: Text(
              'Drafts (${_drafts.length})',
              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
          ),
          if (_drafts.isEmpty)
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 16),
              child: Text('No drafts', style: TextStyle(fontSize: 12)),
            ),
          for (final draft in _drafts)
            _DraftTile(
              draft: draft,
              onOpen: () => _openDraft(draft),
              onDelete: () => _deleteDraft(draft),
            ),
        ],
      ),
    );
  }
}

class _ChoiceTile<T> extends StatelessWidget {
  const _ChoiceTile({
    required this.title,
    required this.value,
    required this.options,
    required this.onChanged,
  });

  final String title;
  final T value;
  final Map<T, String> options;
  final ValueChanged<T> onChanged;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(title),
      trailing: DropdownButton<T>(
        value: value,
        items: [
          for (final entry in options.entries)
            DropdownMenuItem(value: entry.key, child: Text(entry.value)),
        ],
        onChanged: (value) {
          if (value != null) onChanged(value);
        },
      ),
    );
  }
}

class _DraftTile extends StatelessWidget {
  const _DraftTile({required this.draft, required this.onOpen, required this.onDelete});

  final VideoEditorDraft draft;
  final VoidCallback onOpen;
  final VoidCallback onDelete;

  @override
  Widget build(BuildContext context) {
    final hasCover = draft.coverPath.isNotEmpty && File(draft.coverPath).existsSync();
    return ListTile(
      onTap: onOpen,
      leading: ClipRRect(
        borderRadius: BorderRadius.circular(4),
        child: SizedBox(
          width: 36,
          height: 64,
          child: hasCover
              ? Image.file(File(draft.coverPath), fit: BoxFit.cover)
              : const ColoredBox(color: Colors.white12),
        ),
      ),
      title: Text(
        '${draft.clipCount} clip${draft.clipCount == 1 ? '' : 's'} · '
        '${draft.durationSec.toStringAsFixed(1)}s',
      ),
      subtitle: Text(
        'Updated ${draft.updatedAt.toLocal().toString().substring(0, 16)}',
        style: const TextStyle(fontSize: 12),
      ),
      trailing: IconButton(icon: const Icon(Icons.delete_outline), onPressed: onDelete),
    );
  }
}

class _ResultSection extends StatelessWidget {
  const _ResultSection({required this.result, required this.saveLabel, required this.saved});

  final VideoEditorResult result;
  final String saveLabel;
  final _SaveState saved;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Last Result:',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 8),
          if (result.coverPath.isNotEmpty && File(result.coverPath).existsSync())
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.file(File(result.coverPath), height: 200),
            ),
          const SizedBox(height: 8),
          Text('Video: ${result.videoPath}', style: const TextStyle(fontSize: 12)),
          const SizedBox(height: 4),
          Text('Cover: ${result.coverPath}', style: const TextStyle(fontSize: 12)),
          const SizedBox(height: 4),
          Text(
            'Duration: ${result.durationSec.toStringAsFixed(1)}s',
            style: const TextStyle(fontSize: 12),
          ),
          const SizedBox(height: 4),
          Text('Audio: ${result.audioPaths.isEmpty ? '-' : result.audioPaths.join(', ')}', style: const TextStyle(fontSize: 12)),
          const SizedBox(height: 8),
          Text(
            saveLabel,
            style: TextStyle(
              fontSize: 12,
              color: saved == _SaveState.failed ? Colors.redAccent : null,
            ),
          ),
        ],
      ),
    );
  }
}
8
likes
0
points
1.66k
downloads

Documentation

Documentation

Publisher

verified publisherinfinive.dev

Weekly Downloads

Native timeline video editor for Flutter apps on iOS and Android. Presents the editor and returns the rendered MP4.

Homepage
View/report issues

Topics

#video #video-editor #video-editing #video-processing #video-overlay

License

unknown (license)

Dependencies

flutter

More

Packages that depend on flutter_video_editor_plugin

Packages that implement flutter_video_editor_plugin