video_trimmer
用于修剪视频的 Flutter 包。
功能
- 可定制的视频修剪器
- 视频播放控制
- 检索和存储视频文件
还支持转换为 GIF。
修剪编辑器

示例应用

可定制的视频编辑器

用法
- 将依赖项
video_trimmer添加到您的 pubspec.yaml 文件中。
Android
-
转到
<project root>/android/app/build.gradle并设置正确的minSdkVersion,主发布版为 24 或 LTS 版本为 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.0 或 LTS 版本为 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;
});
},
)
],
),
),
),
),
);
}
}