filmkit 0.7.1 copy "filmkit: ^0.7.1" to clipboard
filmkit: ^0.7.1 copied to clipboard

Photo and video editing for Flutter: native export, LUT filters and an Instagram-style editor.

example/lib/main.dart

import 'dart:io';

import 'dart:ui' as ui;

import 'package:filmkit/filmkit.dart';
import 'package:filmkit_picker/filmkit_picker.dart';
import 'package:flutter/material.dart';

import 'sample_looks.dart';
import 'sample_videos.dart';

void main() => runApp(const MaterialApp(home: ExportDemoPage()));

/// Exports a bundled sample video or photo with a few edit presets and an optional film look,
/// previewed with [LutFilter].
class ExportDemoPage extends StatefulWidget {
  const ExportDemoPage({super.key});

  @override
  State<ExportDemoPage> createState() => _ExportDemoPageState();
}

class _ExportDemoPageState extends State<ExportDemoPage> {
  static const _presets = {
    'Trim 1–4 s': EditSpec(trimStart: Duration(seconds: 1), trimEnd: Duration(seconds: 4)),
    'Square crop': EditSpec(crop: Rect.fromLTRB(0.25, 0, 0.75, 1)),
    'Max 320 px': EditSpec(maxDimension: 320),
    'All': EditSpec(trimStart: Duration(seconds: 1), trimEnd: Duration(seconds: 4), crop: Rect.fromLTRB(0.25, 0, 0.75, 1), maxDimension: 320),
  };

  static final _names = [...sampleVideos, 'gradient.jpg', 'oriented_6.jpg', 'gradient.heic'];

  /// Sample files by name.
  Map<String, String>? _media;
  String _name = _names.first;
  bool get _isPhoto => !_name.endsWith('.mp4');
  String _preset = _presets.keys.first;
  String? _inputInfo;
  ui.Image? _frame;
  String? _lookPath;
  bool _look = false;
  double _intensity = 1;

  /// The last editor choices for each sample.
  final _editorStates = <String, EditorState>{};
  bool _exporting = false;
  VideoExport? _export;
  double _progress = 0;
  String? _status;

  @override
  void initState() {
    super.initState();
    Future.wait([copySampleVideos(), copySamplePhotos()]).then((copies) async {
      final lookPath = '${Directory.systemTemp.path}/film_look.cube';
      await File(lookPath).writeAsString(filmLook.encode());
      setState(() {
        _media = {...copies[0], ...copies[1]};
        _lookPath = lookPath;
      });
      _loadInfo();
    });
  }

  Future<void> _loadInfo() async {
    final path = _media![_name]!;
    final String info;
    final ui.Image frame;
    if (_isPhoto) {
      // A small oriented sRGB JPEG decodes everywhere, HEIC included.
      final preview = await Filmkit.exportImage(
        input: path,
        output: '${Directory.systemTemp.path}/filmkit_preview.jpg',
        edit: const EditSpec(maxDimension: 640),
      );
      frame = await _decode(preview.path);
      info = 'Photo';
    } else {
      final video = await Filmkit.getVideoInfo(path);
      frame = await Filmkit.getVideoFrame(path, position: video.duration ~/ 2, maxDimension: 640);
      info = '$video';
    }
    if (!mounted) return frame.dispose();
    final old = _frame;
    setState(() {
      _inputInfo = info;
      _frame = frame;
    });
    WidgetsBinding.instance.addPostFrameCallback((_) => old?.dispose());
  }

  static Future<ui.Image> _decode(String path) async {
    final codec = await ui.instantiateImageCodec(await File(path).readAsBytes());
    final image = (await codec.getNextFrame()).image;
    codec.dispose();
    return image;
  }

  @override
  void dispose() {
    _frame?.dispose();
    super.dispose();
  }

  EditSpec get _edit {
    final preset = _presets[_preset]!;
    return EditSpec(
      trimStart: preset.trimStart,
      trimEnd: preset.trimEnd,
      crop: preset.crop,
      maxDimension: preset.maxDimension,
      lut: _look ? _lookPath : null,
      lutIntensity: _intensity,
    );
  }

  /// Picks a photo or video with filmkit_picker, then edits it: the crop chosen in the picker
  /// becomes the editor's initial crop.
  Future<void> _pickFromGallery() async {
    final results = await FilmkitPicker.pickAndEdit(context);
    if (!mounted || results == null) return;
    final result = results.single;
    setState(() => _status = 'Gallery: ${result.export}\n${result.state}');
  }

  Future<void> _openEditor() async {
    final name = _name;
    final result = await FilmkitEditor.open(context, path: _media![name]!, initialState: _editorStates[name]);
    if (!mounted || result == null) return;
    // Reopening the same sample restores these choices.
    _editorStates[name] = result.state;
    setState(() => _status = 'Editor: ${result.export}\nlook: ${result.look?.name ?? 'none'}, ${result.aspect.label}, ${result.adjustments}');
  }

  Future<void> _run() => _isPhoto ? _runPhoto() : _runVideo();

  Future<void> _runPhoto() async {
    final input = _media![_name]!;
    final output = '${Directory.systemTemp.path}/filmkit_demo_${_name.split('.').first}.jpg';
    setState(() {
      _exporting = true;
      _status = 'Exporting…';
    });
    final watch = Stopwatch()..start();
    String status;
    try {
      final result = await Filmkit.exportImage(input: input, output: output, edit: _edit);
      status = 'Done in ${watch.elapsedMilliseconds} ms\n$result';
    } on FilmkitException catch (e) {
      status = 'Failed: $e';
    }
    if (mounted) {
      setState(() {
        _exporting = false;
        _status = status;
      });
    }
  }

  Future<void> _runVideo() async {
    final input = _media![_name]!;
    final output = '${Directory.systemTemp.path}/filmkit_demo_${_name.replaceAll('.mp4', '')}.mp4';
    final export = Filmkit.exportVideo(input: input, output: output, edit: _edit);
    setState(() {
      _exporting = true;
      _export = export;
      _progress = 0;
      _status = 'Exporting…';
    });
    export.progress.listen((p) => setState(() => _progress = p));
    final watch = Stopwatch()..start();
    String status;
    try {
      final result = await export.result;
      final info = await Filmkit.getVideoInfo(result.path);
      status = 'Done in ${watch.elapsedMilliseconds} ms\n$info\n${result.path}';
    } on FilmkitException catch (e) {
      status = e.code == FilmkitErrorCode.cancelled ? 'Cancelled' : 'Failed: $e';
    }
    if (mounted) {
      setState(() {
        _exporting = false;
        _export = null;
        _status = status;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final busy = _exporting;
    return Scaffold(
      appBar: AppBar(title: const Text('filmkit')),
      body: _media == null
          ? const Center(child: CircularProgressIndicator())
          : ListView(
              padding: const EdgeInsets.all(16),
              children: [
                DropdownMenu<String>(
                  label: const Text('Sample'),
                  initialSelection: _name,
                  enabled: !busy,
                  dropdownMenuEntries: [for (final v in _names) DropdownMenuEntry(value: v, label: v)],
                  onSelected: (v) {
                    setState(() {
                      _name = v!;
                      _inputInfo = null;
                    });
                    _loadInfo();
                  },
                ),
                const SizedBox(height: 8),
                Text(_inputInfo ?? '…'),
                const SizedBox(height: 8),
                if (_frame != null)
                  Center(
                    child: ConstrainedBox(
                      constraints: const BoxConstraints(maxHeight: 240),
                      child: AspectRatio(
                        aspectRatio: _frame!.width / _frame!.height,
                        child: LutFilter(
                          lut: _look ? filmLook : null,
                          intensity: _intensity,
                          child: RawImage(image: _frame, fit: BoxFit.contain),
                        ),
                      ),
                    ),
                  ),
                SwitchListTile(
                  contentPadding: EdgeInsets.zero,
                  title: const Text('Film look'),
                  value: _look,
                  onChanged: busy ? null : (v) => setState(() => _look = v),
                ),
                if (_look)
                  Slider(
                    value: _intensity,
                    label: '${(_intensity * 100).round()} %',
                    divisions: 20,
                    onChanged: busy ? null : (v) => setState(() => _intensity = v),
                  ),
                const SizedBox(height: 16),
                Wrap(
                  spacing: 8,
                  children: [
                    for (final name in _presets.keys)
                      ChoiceChip(label: Text(name), selected: name == _preset, onSelected: busy ? null : (_) => setState(() => _preset = name)),
                  ],
                ),
                const SizedBox(height: 16),
                Row(
                  children: [
                    FilledButton(onPressed: busy ? null : _run, child: const Text('Export')),
                    const SizedBox(width: 8),
                    OutlinedButton(onPressed: _export?.cancel, child: const Text('Cancel')),
                  ],
                ),
                const SizedBox(height: 8),
                FilledButton.tonalIcon(
                  key: const ValueKey('demo.gallery'),
                  onPressed: busy ? null : _pickFromGallery,
                  icon: const Icon(Icons.photo_library),
                  label: const Text('Pick from the gallery'),
                ),
                const SizedBox(height: 8),
                FilledButton.tonalIcon(
                  key: const ValueKey('demo.editor'),
                  onPressed: busy ? null : _openEditor,
                  icon: const Icon(Icons.edit),
                  label: const Text('Open the editor'),
                ),
                const SizedBox(height: 16),
                if (busy) LinearProgressIndicator(value: _export == null ? null : _progress),
                if (_status != null) Padding(padding: const EdgeInsets.only(top: 8), child: Text(_status!)),
              ],
            ),
    );
  }
}
0
likes
160
points
343
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Photo and video editing for Flutter: native export, LUT filters and an Instagram-style editor.

Repository (GitHub)
View/report issues

Topics

#video #image #editor #filter #crop

License

MIT (license)

Dependencies

flutter, plugin_platform_interface, video_player

More

Packages that depend on filmkit

Packages that implement filmkit