6 个月前,我做的项目需要先用视频编辑器处理视频,再将其上传到后端服务器。我第一时间想到的方法就是搜索 Flutter 程序包,但我发现 pub.dev 上没有任何可用于视频编辑的软件包。
但我那时刚开始开发 Flutter app,还没有能力独立构建一个编辑器,于是我决定放弃这个项目。
6 个月后,我惊讶地发现 Flutter 中还是没有可用于视频编辑的程序包,也没有合适的 UI / UX。因此,我决定利用过去几个月学到的技能,为 Flutter 构建一个具有良好 UI 支持的 video trimmer 程序包。
点击此处查看 video_trimmer 程序包:
功能
视频剪辑器的主要功能是:
- 以用户选择的指定格式检索视频文件,并将其存储到文件系统。该软件包支持大多数视频格式,比如 MP4、MKV、MOV、FLV、AVI、WVM 和 GIF;
- 基本的视频播放控件;
- 支持高级 FFmpeg 自定义命令行。
加载和存储
该插件支持大多数视频格式,输入和剪辑后输出的视频可以保存为 MP4、MKV、MOV、FLV、AVI、WVM 和 GIF 等格式。可以从 FileFormat 类中选择格式。
如果你要输出 GIF 格式的文件,还可以选择下列两种格式:
- fpsGIF:为输出 GIF 格式设置 FPS 值。
- scaleGIF:定义输出 GIF 格式的宽度,并根据纵横比缩放高度。
剪辑后的视频可以保存在 StorageDir 类定义的目录中:
- temporaryDirectory:只能从 app 内访问,可随时清除;
- applicationDocumentsDirectory:只能从 app 内部访问;
- externalStorageDirectory:仅支持 Android 平台,可从外部访问。
剪辑后的视频默认为 MP4 格式,存储在你选择的目录的 Trimmer 文件夹中,文件名遵循以下格式:
< original_file_name > _trimmed:< DATE_TIME。> < file_format >
加载视频:
final Trimmer _trimmer = Trimmer();
File _videoFile = await _trimmer.loadVideo();
保存修剪过的视频:
await _trimmer
.saveTrimmedVideo(startValue: _startValue, endValue: _endValue)
.then((value) {
setState(() {
_value = value;
});
});
你还可以自定义文件夹和文件名称,用来存储视频文件。
视频播放控件
这个方法返回一个具有视频播放状态的 boolean 值,确认视频处于播放还是暂停状态。如果你想在播放状态变化后立即触发 app 中的某些操作,这个方法就很有用。
await _trimmer.videPlaybackControl(
startValue: _startValue,
endValue: _endValue,
);
编辑视频
该软件包使用 FFmpeg 命令来剪辑和定义视频的输出格式。
FFmpeg 命令的实施如下:
-i <path_to_video> -ss <start_point> -t <duration> -c copy <output_video_path>/<video_file_name>.<video_format>
- -i:提供输入视频路径;
- -ss:寻找视频的起点;
- -t:指定视频的持续时间;
- -c:为视频提供编译码器。
- copy:一种流拷贝模式的编译码器,省略了指定流的解码和编码步骤,有助于快速进行质量损失转换。
举例来说,一个视频剪辑的起始点为 00:00:03,终点为 00:00:08 (持续时间为 00:00:05),视频输出格式为 .mp4,命令如下:
-i /Photos/video.mp4 -ss 00:00:03 -t 00:00:05 -c copy /Photos/trim_video.mp4
如果想生成一个 GIF 格式的文件,就要使用另一个 FFmpeg 命令,如下:
-i <path_to_video> -ss <start_point> -t <duration> -vf "fps=<fps_of_gif>,scale=<scale_of_gif>:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 <output_video_path>/<video_file_name>.gif
- -vf:为输出视频提供帧数;
- Palettegen 和 Paletteuse 过滤器会从你的输入中生成并使用自定义调色板。
- -loop:指定视频循环播放的次数 。数值 0 是无限循环, -1 是不循环 ,any positive value 会循环指定次数,如果该值为 3,就会把视频播放 4 次。
举例来说,一个视频剪辑的起始点为 00:00:03,终点为 00:00:08 (持续时间就是 00:00:05),将其转换为 .gif 格式(fps 为 10, scale 为 480,循环值为 0,即无限循环),命令如下:
-i /Photos/video.mp4 -ss 00:00:03 -t 00:00:05 -vf "fps=10,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 /Photos/trim_video.gif
该程序包还支持自定义 FFmpeg 命令,具体见下文。
进阶命令
该程序包支持自定义 FFmpeg 命令,可以使用用户定义的视频输出格式来编辑视频,但我们一般不需要使用自定义命令,因为程序包中有我们要用到的大多数视频剪辑功能。
更多信息请参考 FFmpeg 官方文档。
注意:高级选项不提供安全检查,因此,如果 app 内传递了错误的视频格式,可能会导致系统崩溃。
// 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;
});
});
UI / UX
现在,我们开始讲最有趣的部分——视频剪辑器的 UI 设计。
上图背景中的图片来自视频,整个视频被分成若干部分,显示在 TrimEditor 上。
顶角的两个“文本”微件显示了剪辑视频的起始点和终点。
该滑块是通过 GestureDetector 和 CustomPaint 微件实现的。
GestureDetector
我主要使用了 GestureDetector(手势检测)微件的三个回调:
- onHorizontalDragStart
- onHorizontalDragUpdate
- onHorizontalDragEnd
一旦指针触及屏幕上的某些位置,开始水平拖动时,onHorizontalDragStart 就会立即执行回调。我用它来检测指针是在起始点还是终点附近,并相应地移动滑块。
指针运动时,onHorizontalDragUpdate 回调处于活跃状态。在这里,我想给大家设定一个边界,避免滑出视频的起点或终点。我还根据指针的位置,计算了将滑块拖至起点或终点的方向,还确定了要把滑块拖动到起点还是终点。
当指针不再触摸屏幕时,执行 onHorizontalDragEnd 回调。我只用它将圆形支架(位于滑块的两端)设置为正常大小。
我遇到的第一个挑战是,当滑块的起点和终点位于同一位置时,使用上述方法无法实现其运动,就会导致它们永远停在原地。因此,当起点和终点非常靠近时,我必须另外编写算法来使其运动。
这个算法不完美,可能会有一些错误,欢迎大家在 GitHub 资源库上提交 PR,对其进行优化。
CustomPaint
我使用了 CustomPaint 微件来绘制矩形滑块、两个环形支架以及当前视频播放位置 。
对于滑块,我只在画布上使用了 drawRect 方法,传递了双左坐标和右下角坐标来绘制矩形。
var borderPaint = Paint()
..color = borderPaintColor
..strokeWidth = borderWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final rect = Rect.fromPoints(startPos, endPos);
canvas.drawRect(rect, borderPaint);
绘制两个环形支架:
canvas.drawCircle(
startPos + Offset(0, endPos.dy / 2), circleSize, circlePaint);
canvas.drawCircle(
endPos + Offset(0, -endPos.dy / 2), circleSize, circlePaint);
当前视频播放位置:
canvas.drawLine(
currentPos,
currentPos + Offset(0, endPos.dy),
scrubberPaint,
);
TrimEditor 的用户界面是完全可定制的。
总结
该程序包目前还处于测试阶段,可能存在一些错误。欢迎大家随时在 GitHub 上对项目进行改进。
GitHub 资源库链接如下:
https://github.com/sbis04/video_trimmer
视频剪辑器的程序包链接如下:
https://pub.dev/packages/video_trimmer
原文作者:Souvik Biswas
原文链接:https://medium.com/flutter-community/my-journey-building-a-video-trimmer-package-for-flutter-73cd82997a7f