Ente Photos 视频编辑器完全指南:修剪、裁剪、旋转与双引擎导出原理
【免费下载链接】ente💚 End-to-end encrypted cloud for everything.项目地址: https://gitcode.com/GitHub_Trending/en/ente
导读
本文围绕 Ente Photos(本项目 E2EE 云相册)内置的视频编辑器展开,详细讲解其三项核心编辑能力——Trim(修剪)、Crop(裁剪)与Rotate(旋转)的完整使用流程、保存语义,并深入源码层面剖析其「原生导出 + FFmpeg 回退」的双引擎实现原理与输出格式约定。读完本文,你将掌握如何在 iOS/Android 端完成一次不损坏原片的视频编辑,理解裁剪比例背后的坐标系与旋转补偿逻辑,以及导出引擎在何种条件下走 Passthrough(免重编码)路径。
提示:官方说明指出,视频编辑器在v1.2.18版本中经历了重构,大幅提升了大体积视频的编辑速度并优化了用户体验。本文所述功能与源码以当前仓库
main分支为准。
功能总览
视频编辑器让你直接在 Ente Photos 应用内对视频做基础编辑,支持三种主要操作:
| 操作 | 用途 | 关键参数 |
|---|---|---|
| Trim 修剪 | 精确裁切视频长度 | 时间轴滑块设置起止点,最短保留 1 秒 |
| Crop 裁剪 | 调整画面比例或自定义尺寸 | Free / 1:1 / 9:16 / 16:9 / 3:4 / 4:3 |
| Rotate 旋转 | 校正视频方向 | 左旋 90° / 右旋 90° |
所有编辑均以「另存为新副本」的方式落盘,原始视频始终保持不变。
Trim(修剪)
- 使用时间轴滑块设定起点(start)与终点(end),实时预览选区。
- 保存前可反复预览确认所选片段。
- 最短成片时长为 1 秒,由控制器层的
minDuration = const Duration(seconds: 1)硬性约束(见 video_editor_controller.dart)。updateTrim中会先对起止点做[0, videoDuration]范围钳制,若boundedEnd - boundedStart < minDuration则直接拒绝本次修改(见 video_editor_controller.dart)。 - 修剪后的时长在编辑界面实时显示。
值得注意的细节:当视频播放到修剪终点时,控制器会自动跳回起点循环预览(_onVideoChanged中的 trim boundary 处理),方便你反复确认裁剪区间(见 video_editor_controller.dart)。
Crop(裁剪)
裁剪支持固定比例与自由调整两种模式,内置比例在 crop_value.dart 中以枚举CropValue定义,并通过Fraction精确表达比例值:
| 比例 | 适用场景(官方文档说明) | 源码对应枚举 |
|---|---|---|
| Free | 无约束手动调整裁剪框 | CropValue.free(null) |
| 1:1 | 方形格式(如 Instagram 帖子) | CropValue.ratio_1_1(1/1) |
| 9:16 | 竖版格式(如 Instagram Stories、TikTok) | CropValue.ratio_9_16(9/16) |
| 16:9 | 横屏宽幅(如 YouTube、大多数视频) | CropValue.ratio_16_9(16/9) |
| 3:4 | 竖版人像比例 | CropValue.ratio_3_4(3/4) |
| 4:3 | 经典比例 | CropValue.ratio_4_3(4/3) |
注:
CropValue还包含一个original(原片比例)选项,选择后裁剪框重置为全幅(updateCrop(Offset.zero, Offset(1.0, 1.0)))。比例选择 UI 位于 video_crop_page.dart。
裁剪与旋转的联动是此处最核心的实现细节:
- 裁剪框坐标使用归一化坐标系(0.0~1.0),
updateCrop会对四个边界做clamp(0.0, 1.0)校验(见 video_editor_controller.dart)。 - 当你点击某一比例后,
preferredCropAspectRatio被写入,控制器调用_fitCropToDisplayAspectRatio以当前旋转状态修正显示比例:若视频处于 90°/270° 旋转,则内部比例取倒数(sourceRatio = _rotation == 90 || _rotation == 270 ? 1 / displayRatio : displayRatio),并以sourceDisplaySize(来自原生元数据的displayWidth/displayHeight)换算归一化矩形(见 video_editor_controller.dart)。 - 旋转时若已锁定比例,比例值会自动翻转(
_preferredCropAspectRatio = 1 / _preferredCropAspectRatio!),确保横竖屏切换后比例语义仍正确(见 video_editor_controller.dart)。 - 视觉裁剪框与文件坐标之间的换算通过
rotateNormalizedRect完成(见 video_editor_controller.dart),这正是文档所述「裁剪工具自动考虑视频旋转,保证最终输出中比例显示正确」的源码实现。
Rotate(旋转)
- 向左旋转:逆时针 90°
- 向右旋转:顺时针 90°
旋转状态在控制器中以累加的度数保存,rotate90Degrees每次按方向累加 90° 并对 360 取模(见 video_editor_controller.dart)。原生接口层严格限定合法旋转值为90、180、270,非法值会直接抛出ArgumentError(见 native_video_editor.dart)。
如何使用视频编辑器
移动端操作步骤(iOS / Android):
- 在图库中打开任意视频;
- 点击右上角溢出菜单(⋮),选择Edit;
- 编辑器底部会出现三个操作按钮:
- Trim—— 调整视频长度
- Crop—— 改变视频画幅
- Rotate—— 调整视频方向
- 可组合使用一个或多个编辑工具(修剪、裁剪、旋转可叠加应用);
- 点击右上角Save copy(保存副本),生成编辑后的视频。
在 UI 源码层面,这三个入口分别对应VideoTrimPage、VideoCropPage、VideoRotatePage三个子编辑器页面,由主页面 video_editor_page.dart 中的VideoEditorMainActions统一组织,各操作使用assets/video-editor/下的 SVG 图标标识。
保存副本后会发生什么:
- Ente 创建一个应用了全部编辑的新视频文件;
- 原始视频保持原样,不做任何改动;
- 新视频继承以下元数据:
- 原视频的创建时间(
newFile.creationTime = widget.file.creationTime); - 位置数据(优先继承原文件,若缺失则尝试从相册资源异步读取经纬度,见 video_editor_page.dart);
- 所属相册/合集(
newFile.collectionID = widget.file.collectionID);
- 原视频的创建时间(
- 编辑后的视频通过
SyncService.instance.sync()自动同步到你的账号; - 原视频与编辑副本同时出现在图库中。
保存流程的完整链路(以源码为准):导出得到临时 MP4 →PhotoManager.stopChangeNotify()暂停系统相册通知 →PhotoManager.editor.saveVideo写入系统相册 → 构建新的EnteFile并插入本地FilesDB→ 广播LocalPhotosUpdatedEvent刷新界面 → 触发云同步 → 展示成功 Toast 并跳回详情页(见 video_editor_page.dart)。整个过程结束后,临时导出文件会被清理。
视频预览与播放
编辑过程中你可以:
- 在预览区实时查看所有已应用变换(修剪区间、裁剪框、旋转方向叠加生效);
- 使用播放控制条播放/暂停,逐段核对编辑效果;
- 在时间轴上拖动定位,检查任意时间点。
预览区使用VideoEditorPreview+VideoEditorPlayerControl组合实现,套在圆角容器内并带 Hero 动画标签(见 video_editor_page.dart)。预览所呈现的画面与实际导出结果一致——所有变换在保存时按同一套控制器状态(snapshot())计算输出。
技术细节:双引擎导出原理
Ente 为保证不同设备上都能成功保存编辑结果,采用了「原生导出优先,FFmpeg 兜底」的双引擎策略。该逻辑完整实现在 video_editor_page.dart:
- 若
flagService.useNativeVideoEditor开启(默认走原生路径),先尝试原生导出; - 原生导出抛出
NativeVideoEditorException或任何异常时,记录告警日志并把进度归零,自动切换 FFmpeg 再次导出; - 若原生开关关闭,则直接走 FFmpeg。
(内部用户还能在界面上看到一个「Native (i)」调试开关,用于手动切换两条路径对比,见 video_editor_page.dart。)
每条导出路径自身还带有快速失败重试机制:若导出在 1 秒内失败(疑似输出路径瞬时问题),会用新的时间戳输出路径重试一次(见 video_editor_page.dart)。
原生导出(默认)
| 平台 | 底层技术 | 编码策略 |
|---|---|---|
| iOS | AVFoundation(AVMutableComposition+AVAssetExportSession) | HighestQuality 预设,纯修剪时 Passthrough(免重编码) |
| Android | Media3 Transformer(Transformer+EditedMediaItem+Effects) | H.264 编码,纯修剪时optimizeTrim直通 |
- 速度更快、画质保留更好,支持设备硬件加速;
- 输出 MP4(H.264),尽可能保持原始质量。
iOS 侧实现见 VideoExportEngine.swift:通过AVURLAsset读取时长、视频轨与preferredTransform,构建AVMutableComposition后交给AVAssetExportSession,并用异步任务循环上报progress(0~1)驱动进度条。
Android 侧实现见 VideoExportEngine.kt:先解析裁剪区间clipRange,构造MediaItem,将裁剪(Cropeffect)、旋转(ScaleAndRotateTransformation、Presentation)组装为Effects,最终以Composition提交给 Media3Transformer;当effects.isEmpty()且仅修剪时走optimizeTrim直通路径(见 VideoExportEngine.kt)。
Dart 侧的原生能力封装位于 native_video_editor.dart:通过MethodChannel('native_video_editor')调用processVideo方法,入参为inputPath/outputPath与可选的trimStartMs/trimEndMs/rotateDegrees/cropX/cropY/cropWidth/cropHeight,通过EventChannel接收进度,返回结构包含outputPath与isReEncoded标记;同时提供inspectVideo读取原生视频元数据(时长、宽高、旋转角、码率、帧率)用于初始化编辑器。
值得注意的工程细节:当没有任何编辑操作时,原生路径直接复制文件(isReEncoded: false),完全不重编码(见 native_video_export_service.dart);而 FFmpeg 路径同样会依据是否存在裁剪/旋转来决定是否附加-vf滤镜链(见 export_video_service.dart)。
FFmpeg 兜底导出
- 仅在原生导出失败时自动启用;
- 跨平台纯软件编码,兼容性最广;
- 输出 MP4:H.264 视频(libx264)+ AAC 音频。
FFmpeg 兜底命令的完整参数序列由ExportService.createPlan构建(见 export_video_service.dart):
-y -ss <start> -i <input> -t <duration> \ -map 0:v:0 -map 0:a? [-vf <filters>] \ -c:v libx264 -preset ultrafast -pix_fmt yuv420p \ -c:a aac -map_metadata 0 -metadata:s:v:0 rotate=0 \ -movflags +faststart <outputPath>各参数含义:
| 参数 | 作用 |
|---|---|
-ss/-t | 修剪起点与时长(秒) |
-map 0:v:0 -map 0:a? | 只取首路视频流,音频流可选(?表示没有则跳过) |
-vf | 视频滤镜链(crop / rotate 组合),无编辑时不附加 |
-c:v libx264 -preset ultrafast | H.264 软件编码,ultrafast 预设换取速度 |
-pix_fmt yuv420p | 保证最大播放器兼容性 |
-c:a aac | AAC 音频编码 |
-map_metadata 0 | 保留原文件元数据 |
-metadata:s:v:0 rotate=0 | 输出中清空旋转标记(旋转已烘焙进画面) |
-movflags +faststart | 将 moov 元数据前置,便于流式播放 |
FFmpeg 路径通过ffmpeg_kit_flutter的executeWithArgumentsAsync异步执行,并校验返回码与输出文件非空(见 export_video_service.dart)。
输出格式
所有编辑后的视频统一导出为MP4文件:
- 文件命名:
[原文件名]_edited_[时间戳].mp4。源码中时间戳取DateTime.now().microsecondsSinceEpoch的微秒级数值以保证唯一性,随后再经getMediaStoreCompatibleTitle处理为相册兼容名称(见 video_editor_page.dart); - 分辨率:尽可能保留原始分辨率(原生路径下尤其如此);
- 画质:原生导出保持最高质量;FFmpeg 兜底采用标准 H.264/AAC 编码。
处理时间与进度
导出耗时取决于以下因素:
- 视频时长与分辨率;
- 应用的编辑项数量(修剪、裁剪、旋转的组合);
- 设备处理性能;
- 导出引擎(原生 vs FFmpeg)。
导出过程中会弹出进度对话框(LinearProgressDialog,标题为「Saving edits」),进度由各引擎的onProgress回调实时刷新;原生导出失败切换 FFmpeg 时会先把进度重置为 0(见 video_editor_page.dart)。
限制与注意事项
- 最短时长:修剪后视频必须至少 1 秒(控制器初始化时若原视频不足 1 秒,会抛出
VideoMinimumDurationError并直接关闭编辑器,见 video_editor_controller.dart); - 仅限移动端:目前仅 iOS 与 Android 应用可用,桌面端与 Web 端不提供该编辑器;
- 单视频编辑:一次只能编辑一个视频;
- 破坏性工作流:无法保存/复用编辑预设——每次导出都会产出一个最终视频成品(编辑器虽内置
snapshot()/restore()状态快照用于页面切换时的状态保持,但不存在可跨会话保存的编辑方案)。
源码导读
如果你想继续深入,可以从以下文件切入:
- 编辑器主页面与导出编排:video_editor_page.dart
- 编辑器状态控制器(修剪/裁剪/旋转的状态机):video_editor_controller.dart
- 裁剪比例定义:crop_value.dart、裁剪页面 video_crop_page.dart
- 原生导出封装(Dart 层):native_video_editor.dart、native_video_export_service.dart
- FFmpeg 导出计划:export_video_service.dart
- Android 原生引擎:VideoExportEngine.kt(另有
VideoClipRangeUsTest.kt、VideoModelsTest.kt单元测试) - iOS 原生引擎:VideoExportEngine.swift(配套 VideoTransformPlannerTests.swift)
- 端到端验证:video_editor_test.dart(覆盖 Trim 组合、Native/FFmpeg 导出对比等场景)、video_editor_controller_test.dart 与 video_editor_contract_test.dart
上述测试用例直接验证了本文描述的众多行为,例如:修剪起点偏移与时长组合、裁剪矩形计算、旋转角度与比例翻转、导出成功/失败与回退路径等,可作为阅读源码时的参照。
【免费下载链接】ente💚 End-to-end encrypted cloud for everything.项目地址: https://gitcode.com/GitHub_Trending/en/ente
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考