构建 Flutter 视频剪辑器程序包

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、WVMGIF 等格式。可以从 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:为输出视频提供帧数;
  • PalettegenPaletteuse 过滤器会从你的输入中生成使用自定义调色板。
  • -loop:指定视频循环播放的次数 。数值 0 是无限循环, -1 是不循环 ,any positive value 会循环指定次数,如果该值为 3,就会把视频播放 4 次。

举例来说,一个视频剪辑的起始点00:00:03终点00:00:08持续时间就是 00:00:05),将其转换为 .gif 格式(fps10scale480,循环值为 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 上。

顶角的两个“文本”微件显示了剪辑视频的起始点终点

该滑块是通过 GestureDetectorCustomPaint 微件实现的。


GestureDetector

我主要使用了 GestureDetector(手势检测)微件的三个回调:

  • onHorizo​​ntalDragStart
  • onHorizo​​ntalDragUpdate
  • onHorizo​​ntalDragEnd

一旦指针触及屏幕上的某些位置,开始水平拖动时,onHorizo​​ntalDragStart 就会立即执行回调。我用它来检测指针是在起始点还是终点附近,并相应地移动滑块。

指针运动时,onHorizo​​ntalDragUpdate 回调处于活跃状态。在这里,我想给大家设定一个边界,避免滑出视频的起点或终点。我还根据指针的位置,计算了将滑块拖至起点终点的方向,还确定了要把滑块拖动到起点还是终点

当指针不再触摸屏幕时,执行 onHorizo​​ntalDragEnd 回调。我只用它将圆形支架(位于滑块的两端)设置为正常大小。

我遇到的第一个挑战是,当滑块的起点终点位于同一位置时,使用上述方法无法实现其运动,就会导致它们永远停在原地。因此,当起点和终点非常靠近时,我必须另外编写算法来使其运动。

这个算法不完美,可能会有一些错误,欢迎大家在 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
推荐阅读
相关专栏
前端与跨平台
90 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。