Skip to main content

flutter_video_editor_plugin 1.1.0 copy "flutter_video_editor_plugin: ^1.1.0" to clipboard
flutter_video_editor_plugin: ^1.1.0 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.

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

Timeline-based multi-clip video editor: trim, split, stickers, text, color filters, transitions and audio. Preview and export share one native composition.

Homepage

License

unknown (license)

Dependencies

flutter

More

Packages that depend on flutter_video_editor_plugin

Packages that implement flutter_video_editor_plugin