video_trimmer

用于修剪视频的 Flutter 包。

功能

  • 可定制的视频修剪器
  • 视频播放控制
  • 检索和存储视频文件

还支持转换为 GIF

修剪编辑器

editor_demo

示例应用

trimmer

可定制的视频编辑器

trim_editor

用法

  • 将依赖项 video_trimmer 添加到您的 pubspec.yaml 文件中。

Android

  • 转到 <project root>/android/app/build.gradle 并设置正确的 minSdkVersion主发布版24LTS 版本16

    请参阅 FFmpeg 版本 部分。

    minSdkVersion <version>
    
  • 转到 <project root>/android/build.gradle 并添加以下行

    ext.flutterFFmpegPackage = '<package name>'
    

    <package name> 替换为 包列表 部分中的正确包名。

iOS

  • 将以下键添加到您的 Info.plist 文件中,该文件位于 <project root>/ios/Runner/Info.plist

    <key>NSCameraUsageDescription</key>
    <string>Used to demonstrate image picker plugin</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>Used to capture audio for image picker plugin</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>Used to demonstrate image picker plugin</string>
    
  • ios/Podfile 中设置平台版本,主发布版11.0LTS 版本9.3

    请参阅 FFmpeg 版本 部分。

    platform :ios, '<version>'
    
  • [Flutter >= 1.20.x] 编辑 ios/Podfile 并在 target 'Runner' do 部分之前添加以下块

    def flutter_install_ios_plugin_pods(ios_application_path = nil)
      # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
      ios_application_path ||= File.dirname(defined_in_file.realpath) if self.respond_to?(:defined_in_file)
      raise 'Could not find iOS application path' unless ios_application_path
    
      # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
      # referring to absolute paths on developers' machines.
    
      symlink_dir = File.expand_path('.symlinks', ios_application_path)
      system('rm', '-rf', symlink_dir) # Avoid the complication of dependencies like FileUtils.
    
      symlink_plugins_dir = File.expand_path('plugins', symlink_dir)
      system('mkdir', '-p', symlink_plugins_dir)
    
      plugins_file = File.join(ios_application_path, '..', '.flutter-plugins-dependencies')
      plugin_pods = flutter_parse_plugins_file(plugins_file)
      plugin_pods.each do |plugin_hash|
        plugin_name = plugin_hash['name']
        plugin_path = plugin_hash['path']
        if (plugin_name && plugin_path)
          symlink = File.join(symlink_plugins_dir, plugin_name)
          File.symlink(plugin_path, symlink)
    
          if plugin_name == 'flutter_ffmpeg'
              pod 'flutter_ffmpeg/<package name>', :path => File.join('.symlinks', 'plugins', plugin_name, 'ios')
          else
              pod plugin_name, :path => File.join('.symlinks', 'plugins', plugin_name, 'ios')
          end
        end
      end
    end
    

    <package name> 替换为 包列表 部分中的正确包名。

  • [Flutter < 1.20.x] 编辑 ios/Podfile 文件并按如下方式修改默认的 # Plugin Pods 块。

    # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
    # referring to absolute paths on developers' machines.
    
    system('rm -rf .symlinks')
    system('mkdir -p .symlinks/plugins')
    plugin_pods = parse_KV_file('../.flutter-plugins')
    plugin_pods.each do |name, path|
      symlink = File.join('.symlinks', 'plugins', name)
      File.symlink(path, symlink)
      if name == 'flutter_ffmpeg'
          pod name+'/<package name>', :path => File.join(symlink, 'ios')
      else
          pod name, :path => File.join(symlink, 'ios')
      end
    end
    

    <package name> 替换为 包列表 部分中的正确包名。

FFmpeg 版本

参考 flutter_ffmpeg 包中指定的版本。

主发布版 LTS 发行版
Android API 级别 24 16
Android 相机访问 -
Android 架构 arm-v7a-neon
arm64-v8a
x86
x86-64
arm-v7a
arm-v7a-neon
arm64-v8a
x86
x86-64
Xcode 支持 10.1 7.3.1
iOS SDK 12.1 9.3
iOS 架构 arm64
arm64e
x86-64
armv7
arm64
i386
x86-64

包列表

以下FFmpeg包列表参考了 flutter_ffmpeg 包。

主发布版 LTS 发行版
min min min-lts
min-gpl min-gpl min-gpl-lts
https https https-lts
https-gpl https-gpl https-gpl-lts
audio audio audio-lts
video video video-lts
full full full-lts
full-gpl full-gpl full-gpl-lts

功能

加载输入视频文件

final Trimmer _trimmer = Trimmer();
await _trimmer.loadVideo(videoFile: file);

保存修剪后的视频

返回一个字符串,指示保存操作是否成功。

await _trimmer
    .saveTrimmedVideo(startValue: _startValue, endValue: _endValue)
    .then((value) {
  setState(() {
    _value = value;
  });
});

视频播放状态

返回视频播放状态。如果为true,则视频正在播放,否则为暂停。

await _trimmer.videPlaybackControl(
  startValue: _startValue,
  endValue: _endValue,
);

高级命令

如果您需要更多自定义,可以使用高级FFmpeg命令。只需使用 ffmpegCommand 属性定义您的 FFmpeg 命令,并使用 customVideoFormat 设置输出视频格式。

有关更多信息,请参阅 官方 FFmpeg 文档

注意: 将错误的视频格式传递给 customVideoFormat 属性可能会导致崩溃。

// Example of defining a custom command

// This is already used for creating GIF by
// default, so you do not need to use this.

await _trimmer
    .saveTrimmedVideo(
        startValue: _startValue,
        endValue: _endValue,
        ffmpegCommand:
            '-vf "fps=10,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0',
        customVideoFormat: '.gif')
    .then((value) {
  setState(() {
    _value = value;
  });
});

小部件

显示视频播放区域

VideoViewer()

显示视频修剪区域

TrimEditor(
  viewerHeight: 50.0,
  viewerWidth: MediaQuery.of(context).size.width,
  onChangeStart: (value) {
    _startValue = value;
  },
  onChangeEnd: (value) {
    _endValue = value;
  },
  onChangePlaybackState: (value) {
    setState(() {
      _isPlaying = value;
    });
  },
)

示例

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_trimmer/trim_editor.dart';
import 'package:video_trimmer/video_trimmer.dart';
import 'package:video_trimmer/video_viewer.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Trimmer',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  final Trimmer _trimmer = Trimmer();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Video Trimmer"),
      ),
      body: Center(
        child: Container(
          child: RaisedButton(
            child: Text("LOAD VIDEO"),
            onPressed: () async {
              File file = await ImagePicker.pickVideo(
                source: ImageSource.gallery,
              );
              if (file != null) {
                await _trimmer.loadVideo(videoFile: file);
                Navigator.of(context)
                    .push(MaterialPageRoute(builder: (context) {
                  return TrimmerView(_trimmer);
                }));
              }
            },
          ),
        ),
      ),
    );
  }
}

class TrimmerView extends StatefulWidget {
  final Trimmer _trimmer;
  TrimmerView(this._trimmer);
  @override
  _TrimmerViewState createState() => _TrimmerViewState();
}

class _TrimmerViewState extends State<TrimmerView> {
  double _startValue = 0.0;
  double _endValue = 0.0;

  bool _isPlaying = false;
  bool _progressVisibility = false;

  Future<String> _saveVideo() async {
    setState(() {
      _progressVisibility = true;
    });

    String _value;

    await widget._trimmer
        .saveTrimmedVideo(startValue: _startValue, endValue: _endValue)
        .then((value) {
      setState(() {
        _progressVisibility = false;
        _value = value;
      });
    });

    return _value;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Video Trimmer"),
      ),
      body: Builder(
        builder: (context) => Center(
          child: Container(
            padding: EdgeInsets.only(bottom: 30.0),
            color: Colors.black,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              mainAxisSize: MainAxisSize.max,
              children: <Widget>[
                Visibility(
                  visible: _progressVisibility,
                  child: LinearProgressIndicator(
                    backgroundColor: Colors.red,
                  ),
                ),
                RaisedButton(
                  onPressed: _progressVisibility
                      ? null
                      : () async {
                          _saveVideo().then((outputPath) {
                            print('OUTPUT PATH: $outputPath');
                            final snackBar = SnackBar(content: Text('Video Saved successfully'));
                            Scaffold.of(context).showSnackBar(snackBar);
                          });
                        },
                  child: Text("SAVE"),
                ),
                Expanded(
                  child: VideoViewer(),
                ),
                Center(
                  child: TrimEditor(
                    viewerHeight: 50.0,
                    viewerWidth: MediaQuery.of(context).size.width,
                    onChangeStart: (value) {
                      _startValue = value;
                    },
                    onChangeEnd: (value) {
                      _endValue = value;
                    },
                    onChangePlaybackState: (value) {
                      setState(() {
                        _isPlaying = value;
                      });
                    },
                  ),
                ),
                FlatButton(
                  child: _isPlaying
                      ? Icon(
                          Icons.pause,
                          size: 80.0,
                          color: Colors.white,
                        )
                      : Icon(
                          Icons.play_arrow,
                          size: 80.0,
                          color: Colors.white,
                        ),
                  onPressed: () async {
                    bool playbackState =
                        await widget._trimmer.videPlaybackControl(
                      startValue: _startValue,
                      endValue: _endValue,
                    );
                    setState(() {
                      _isPlaying = playbackState;
                    });
                  },
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

GitHub

https://github.com/sbis04/video_trimmer