Flutter视频编辑插件video_editor_pits的使用
Flutter视频编辑插件video_editor_pits的使用
Flutter视频编辑插件
video_editor_pits
是一个视频编辑库,允许用户对视频进行裁剪、旋转、缩放等操作,并可以选择封面。该库提供了执行导出的一些工具,但不处理导出。
该库仅用 Dart 编写,但使用了外部包如 video_thumbnail
,使其目前仅支持 iOS 和 Android 平台(web 支持正在进行中)。
注意
如果你使用的是 1.2.3 到 2.4.0 之间的版本,你的项目可能受到 GPL 许可证的影响。
Android | iOS | |
---|---|---|
支持 | SDK 16+ | 11.0+ |
安装
以下是将此库添加到 Flutter 项目的步骤:
- 在终端运行
flutter pub add video_editor
,或手动在pubspec.yaml
文件中添加依赖项。
dependencies:
video_editor: ^2.4.0
- 在代码中导入该包:
import 'package:video_editor_pits/video_editor.dart';
截图
示例应用运行在 iPhone 11 Pro 上 | 自定义示例,浅色模式 |
---|---|
![]() |
![]() |
使用
以下是一个完整的示例代码,展示了如何使用 video_editor_pits
插件来编辑视频:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_editor/video_editor.dart';
void main() => runApp(
MaterialApp(
title: 'Flutter Video Editor Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.grey,
brightness: Brightness.dark,
tabBarTheme: const TabBarTheme(
indicator: UnderlineTabIndicator(
borderSide: BorderSide(color: Colors.white),
),
),
dividerColor: Colors.white,
),
home: const VideoEditorExample(),
),
);
class VideoEditorExample extends StatefulWidget {
const VideoEditorExample({super.key});
[@override](/user/override)
State<VideoEditorExample> createState() => _VideoEditorExampleState();
}
class _VideoEditorExampleState extends State<VideoEditorExample> {
final ImagePicker _picker = ImagePicker();
void _pickVideo() async {
final XFile? file = await _picker.pickVideo(source: ImageSource.gallery);
if (mounted && file != null) {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) => VideoEditor(file: File(file.path)),
),
);
}
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Video Picker")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("点击按钮选择视频"),
ElevatedButton(
onPressed: _pickVideo,
child: const Text("从相册选择视频"),
),
],
),
),
);
}
}
//-------------------//
//VIDEO EDITOR SCREEN//
//-------------------//
class VideoEditor extends StatefulWidget {
const VideoEditor({super.key, required this.file});
final File file;
[@override](/user/override)
State<VideoEditor> createState() => _VideoEditorState();
}
class _VideoEditorState extends State<VideoEditor> {
final _exportingProgress = ValueNotifier<double>(0.0);
final _isExporting = ValueNotifier<bool>(false);
final double height = 60;
late final VideoEditorController _controller = VideoEditorController.file(
widget.file,
minDuration: const Duration(seconds: 1),
maxDuration: const Duration(seconds: 10),
);
[@override](/user/override)
void initState() {
super.initState();
_controller
.initialize(aspectRatio: 9 / 16)
.then((_) => setState(() {}))
.catchError((error) {
// 处理最小持续时间大于视频持续时间错误
Navigator.pop(context);
}, test: (e) => e is VideoMinDurationError);
}
[@override](/user/override)
void dispose() async {
_exportingProgress.dispose();
_isExporting.dispose();
_controller.dispose();
super.dispose();
}
void _showErrorSnackBar(String message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 1),
),
);
void _exportVideo() async {
_exportingProgress.value = 0;
_isExporting.value = true;
final config = VideoFFmpegVideoEditorConfig(
_controller,
// format: VideoExportFormat.gif,
// commandBuilder: (config, videoPath, outputPath) {
// final List<String> filters = config.getExportFilters();
// filters.add('hflip'); // 添加水平翻转
// return '-i $videoPath ${config.filtersCmd(filters)} -preset ultrafast $outputPath';
// },
);
await ExportService.runFFmpegCommand(
await config.getExecuteConfig(),
onProgress: (stats) {
_exportingProgress.value = config.getFFmpegProgress(stats.getTime());
},
onError: (e, s) => _showErrorSnackBar("导出视频时发生错误 :("),
onCompleted: (file) {
_isExporting.value = false;
if (!mounted) return;
showDialog(
context: context,
builder: (_) => VideoResultPopup(video: file),
);
},
);
}
void _exportCover() async {
final config = CoverFFmpegVideoEditorConfig(_controller);
final execute = await config.getExecuteConfig();
if (execute == null) {
_showErrorSnackBar("导出封面初始化错误。");
return;
}
await ExportService.runFFmpegCommand(
execute,
onError: (e, s) => _showErrorSnackBar("导出封面时发生错误 :("),
onCompleted: (cover) {
if (!mounted) return;
showDialog(
context: context,
builder: (_) => CoverResultPopup(cover: cover),
);
},
);
}
[@override](/user/override)
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
backgroundColor: Colors.black,
body: _controller.initialized
? SafeArea(
child: Stack(
children: [
Column(
children: [
_topNavBar(),
Expanded(
child: DefaultTabController(
length: 2,
child: Column(
children: [
Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
Stack(
alignment: Alignment.center,
children: [
CropGridViewer.preview(
controller: _controller),
AnimatedBuilder(
animation: _controller.video,
builder: (_, __) => AnimatedOpacity(
opacity:
_controller.isPlaying ? 0 : 1,
duration: kThemeAnimationDuration,
child: GestureDetector(
onTap: _controller.video.play,
child: Container(
width: 40,
height: 40,
decoration:
const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
color: Colors.black,
),
),
),
),
),
],
),
CoverViewer(controller: _controller)
],
),
),
Container(
height: 200,
margin: const EdgeInsets.only(top: 10),
child: Column(
children: [
const TabBar(
tabs: [
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(5),
child: Icon(Icons.content_cut)),
Text('Trim')
]),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(5),
child: Icon(Icons.video_label)),
Text('Cover')
],
),
],
),
Expanded(
child: TabBarView(
physics:
const NeverScrollableScrollPhysics(),
children: [
Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: _trimSlider(),
),
_coverSelection(),
],
),
),
],
),
),
ValueListenableBuilder(
valueListenable: _isExporting,
builder: (_, bool export, Widget? child) =>
AnimatedSize(
duration: kThemeAnimationDuration,
child: export ? child : null,
),
child: AlertDialog(
title: ValueListenableBuilder(
valueListenable: _exportingProgress,
builder: (_, double value, __) => Text(
"导出视频 ${(value * 100).ceil()}%",
style: const TextStyle(fontSize: 12),
),
),
),
)
],
),
),
)
],
)
],
),
)
: const Center(child: CircularProgressIndicator()),
),
);
}
Widget _topNavBar() {
return SafeArea(
child: SizedBox(
height: height,
child: Row(
children: [
Expanded(
child: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.exit_to_app),
tooltip: '离开编辑器',
),
),
const VerticalDivider(endIndent: 22, indent: 22),
Expanded(
child: IconButton(
onPressed: () =>
_controller.rotate90Degrees(RotateDirection.left),
icon: const Icon(Icons.rotate_left),
tooltip: '逆时针旋转',
),
),
Expanded(
child: IconButton(
onPressed: () =>
_controller.rotate90Degrees(RotateDirection.right),
icon: const Icon(Icons.rotate_right),
tooltip: '顺时针旋转',
),
),
Expanded(
child: IconButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => CropPage(controller: _controller),
),
),
icon: const Icon(Icons.crop),
tooltip: '打开裁剪屏幕',
),
),
const VerticalDivider(endIndent: 22, indent: 22),
Expanded(
child: PopupMenuButton(
tooltip: '打开导出菜单',
icon: const Icon(Icons.save),
itemBuilder: (context) => [
PopupMenuItem(
onTap: _exportCover,
child: const Text('导出封面'),
),
PopupMenuItem(
onTap: _exportVideo,
child: const Text('导出视频'),
),
],
),
),
],
),
),
);
}
String formatter(Duration duration) =>
[
duration.inMinutes.remainder(60).toString().padLeft(2, '0'),
duration.inSeconds.remainder(60).toString().padLeft(2, '0')
].join(":");
List<Widget> _trimSlider() {
return [
AnimatedBuilder(
animation: Listenable.merge([
_controller,
_controller.video,
]),
builder: (_, __) {
final int duration = _controller.videoDuration.inSeconds;
final double pos = _controller.trimPosition * duration;
return Padding(
padding: EdgeInsets.symmetric(horizontal: height / 4),
child: Row(children: [
Text(formatter(Duration(seconds: pos.toInt()))),
const Expanded(child: SizedBox()),
AnimatedOpacity(
opacity: _controller.isTrimming ? 1 : 0,
duration: kThemeAnimationDuration,
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(formatter(_controller.startTrim)),
const SizedBox(width: 10),
Text(formatter(_controller.endTrim)),
]),
),
]),
);
},
),
Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.symmetric(vertical: height / 4),
child: TrimSlider(
controller: _controller,
height: height,
horizontalMargin: height / 4,
child: TrimTimeline(
controller: _controller,
padding: const EdgeInsets.only(top: 10),
),
),
)
];
}
Widget _coverSelection() {
return SingleChildScrollView(
child: Center(
child: Container(
margin: const EdgeInsets.all(15),
child: CoverSelection(
controller: _controller,
size: height + 10,
quantity: 8,
selectedCoverBuilder: (cover, size) {
return Stack(
alignment: Alignment.center,
children: [
cover,
Icon(
Icons.check_circle,
color: const CoverSelectionStyle().selectedBorderColor,
)
],
);
},
),
),
),
);
}
}
更多关于Flutter视频编辑插件video_editor_pits的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter视频编辑插件video_editor_pits的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
video_editor_pits
是一个用于 Flutter 的视频编辑插件,它可以帮助你在 Flutter 应用中实现视频裁剪、压缩、添加水印等功能。以下是如何使用 video_editor_pits
插件的步骤:
1. 添加依赖
首先,你需要在 pubspec.yaml
文件中添加 video_editor_pits
插件的依赖:
dependencies:
flutter:
sdk: flutter
video_editor_pits: ^1.0.0 # 请使用最新版本
然后运行 flutter pub get
来获取依赖。
2. 导入插件
在你的 Dart 文件中导入 video_editor_pits
插件:
import 'package:video_editor_pits/video_editor_pits.dart';
3. 初始化插件
在使用插件之前,你需要先初始化它:
VideoEditorPits videoEditor = VideoEditorPits();
4. 裁剪视频
你可以使用 cropVideo
方法来裁剪视频。以下是一个示例:
String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
int startTime = 1000; // 开始时间(毫秒)
int endTime = 5000; // 结束时间(毫秒)
videoEditor.cropVideo(
inputPath: inputPath,
outputPath: outputPath,
startTime: startTime,
endTime: endTime,
).then((result) {
if (result) {
print('视频裁剪成功');
} else {
print('视频裁剪失败');
}
});
5. 压缩视频
你可以使用 compressVideo
方法来压缩视频。以下是一个示例:
String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
int quality = 50; // 质量百分比(0-100)
videoEditor.compressVideo(
inputPath: inputPath,
outputPath: outputPath,
quality: quality,
).then((result) {
if (result) {
print('视频压缩成功');
} else {
print('视频压缩失败');
}
});
6. 添加水印
你可以使用 addWatermark
方法来为视频添加水印。以下是一个示例:
String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
String watermarkPath = '/path/to/watermark.png'; // 水印图片路径
WatermarkPosition position = WatermarkPosition.bottomRight; // 水印位置
videoEditor.addWatermark(
inputPath: inputPath,
outputPath: outputPath,
watermarkPath: watermarkPath,
position: position,
).then((result) {
if (result) {
print('水印添加成功');
} else {
print('水印添加失败');
}
});
7. 处理视频
你可以使用 processVideo
方法来同时进行裁剪、压缩和添加水印。以下是一个示例:
String inputPath = '/path/to/input/video.mp4';
String outputPath = '/path/to/output/video.mp4';
int startTime = 1000; // 开始时间(毫秒)
int endTime = 5000; // 结束时间(毫秒)
int quality = 50; // 质量百分比(0-100)
String watermarkPath = '/path/to/watermark.png'; // 水印图片路径
WatermarkPosition position = WatermarkPosition.bottomRight; // 水印位置
videoEditor.processVideo(
inputPath: inputPath,
outputPath: outputPath,
startTime: startTime,
endTime: endTime,
quality: quality,
watermarkPath: watermarkPath,
position: position,
).then((result) {
if (result) {
print('视频处理成功');
} else {
print('视频处理失败');
}
});
8. 处理异常
在使用插件时,可能会遇到各种异常。你可以使用 try-catch
来捕获并处理这些异常:
try {
await videoEditor.cropVideo(
inputPath: inputPath,
outputPath: outputPath,
startTime: startTime,
endTime: endTime,
);
} catch (e) {
print('发生异常: $e');
}