Flutter for OpenHarmony 文件转换助手App实战 - 视频转换功能

视频转换功能允许用户将视频文件在不同格式之间进行转换,如MP4、AVI、MOV和MKV。本文将详细讲解如何实现视频转换功能。
本文中的代码来自本目录下的最小可运行示例工程(lib/ 目录)。你可以把它当作“视频转换功能”在项目里的第一版落地:先把 UI、参数收集、任务进度这些流程跑通,再逐步接入真实的转码实现(比如通过平台侧能力或命令行工具)。
视频转换功能的设计
视频转换功能包括两个主要操作:格式转换和视频压缩。这两个功能都需要处理视频的分辨率和帧率参数。
在转换页面中,我们定义了视频转换的两个功能:
Widget _buildVideoTab() {
return ListView(
padding: const EdgeInsets.all(16),
children: [
ConvertItem(
icon: Icons.video_file,
title: '视频格式转换',
subtitle: 'MP4/AVI/MOV/MKV',
onTap: () => _showConvertDetail('视频格式转换'),
),
const SizedBox(height: 12),
ConvertItem(
icon: Icons.videocam,
title: '视频压缩',
subtitle: '减小视频文件大小',
onTap: () => _showConvertDetail('视频压缩'),
),
],
);
}
说明
这里我习惯先把入口做成“两个大按钮”,原因很现实:
- 需求沟通更顺:产品/同学看到页面就能明确“格式转换”和“压缩”是两条链路。
- 后续扩展不费劲:压缩本质上也是转码,只是默认把分辨率/码率往下调,你把入口拆开后,参数默认值就好管理。
- 事件入口统一:
onTap都进_showConvertDetail(...),后面可以很自然地加埋点、权限检查、文件选择器等逻辑。
视频格式的支持
应用支持多种视频格式的转换,包括MP4、AVI、MOV和MKV。每种格式都有不同的编码方式和兼容性。
格式在工程里我用 enum 固定下来,避免 UI 层直接拿字符串到处拼(后面做国际化、做兼容判断也方便):
enum VideoFormat {
mp4,
avi,
mov,
mkv,
}
说明
enum 看起来“多写了点”,但实际能省很多排查时间:
- 减少无效输入:不会出现
MP-4、Mp4这类字符串差异导致的 bug。 - 便于做映射:真接入转码层时,你最终还是要把
VideoFormat.mp4映射成具体参数(比如-f mp4),集中处理更清晰。 - 默认值更好选:UI 初始化默认选
mp4,比默认一个“选择格式”字符串更稳。
DropdownButtonFormField<VideoFormat>(
value: _format,
decoration: const InputDecoration(
labelText: '输出格式',
border: OutlineInputBorder(),
),
items: VideoFormat.values
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.name.toUpperCase()),
),
)
.toList(),
onChanged: (value) => setState(() => _format = value ?? _format),
)
说明
这个下拉框不是重点,重点是它在“真实项目”里要考虑两件事:
- 有默认值:页面第一次打开就能直接点“开始”,体验会顺很多。
- 值变化要落到状态:这里用
setState存在_format上,后面组装任务参数时就不会“读 UI 再解析”。
视频质量的设置
视频转换通常需要设置分辨率、帧率和编码器。分辨率决定了视频的清晰度,帧率决定了视频的流畅度。
分辨率同样用 enum 来约束:
enum VideoResolution {
p480,
p720,
p1080,
k2,
k4,
}
说明
分辨率这个字段我建议一开始就“收紧”一点:
- 先做常见档位:真实用户更关心“高清/超清”这种档位,不太会手填宽高。
- 避免参数组合爆炸:自由输入宽高会牵出更多校验、比例、旋转、黑边的问题。
DropdownButtonFormField<VideoResolution>(
value: _resolution,
decoration: const InputDecoration(
labelText: '分辨率',
border: OutlineInputBorder(),
),
items: VideoResolution.values
.map(
(e) => DropdownMenuItem(value: e, child: Text(e.name)),
)
.toList(),
onChanged: (value) => setState(() => _resolution = value ?? _resolution),
)
说明
这里的 e.name 是为了让 demo 更短;如果是正式项目,我一般会:
- 单独做一个显示文案(比如
p1080显示为1080p),避免 UI 绑死在枚举命名上。 - 预留“跟随原始分辨率”的选项(尤其是做压缩时),否则你会遇到用户抱怨“为什么我不能只降帧率”。
帧率的选择
帧率是视频流畅度的重要指标。常见的帧率有24fps、30fps和60fps。
DropdownButtonFormField<int>(
value: _fps,
decoration: const InputDecoration(
labelText: '帧率',
border: OutlineInputBorder(),
),
items: const [24, 30, 60]
.map((e) => DropdownMenuItem(value: e, child: Text('${e}fps')))
.toList(),
onChanged: (value) => setState(() => _fps = value ?? _fps),
)
说明
帧率这块最常见的坑其实是“输入视频帧率更低怎么办”。我自己的经验是:
- UI 这里选的是“目标帧率”,底层执行时应该做一次兜底(比如输入只有 25fps,你选 60fps 就没意义)。
- 做压缩功能时,帧率和分辨率不要同时默认拉太低,不然用户第一反应是“画面糊 + 卡”。
视频压缩的实现
视频压缩功能允许用户降低视频的分辨率和帧率,从而减小文件大小。这对于存储和传输大型视频文件非常有用。
弹窗我拆成了一个小组件 _ConvertDialog,并用一个结果对象把参数收回来:
final result = await showDialog<_ConvertDialogResult>(
context: context,
builder: (context) => _ConvertDialog(title: title),
);
说明
这么写的好处是“干净”:
- 页面只负责打开弹窗 + 接收结果,不需要关心弹窗内部怎么管理状态。
- 以后你想把弹窗改成独立页面(甚至抽成可复用组件)时,调用方式基本不用动。
TextButton(
onPressed: () {
final path = _pathController.text.trim();
if (path.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请先填写输入文件路径')),
);
return;
}
Navigator.pop(
context,
_ConvertDialogResult(
inputPath: path,
format: _format,
resolution: _resolution,
fps: _fps,
codec: _codec,
),
);
},
child: const Text('开始'),
)
说明
这段看起来像“只是个按钮”,但它解决了两个很项目化的问题:
- 校验尽量靠近输入源头:路径为空就直接拦在弹窗里,别把无效参数传到转换层再报错。
- 用对象回传参数:
_ConvertDialogResult把参数一次性带出去,调用方不用从多个状态变量里“捡”。
编码器的选择
不同的编码器有不同的压缩效率和兼容性。常见的视频编码器有H.264和H.265。
编码器同样做成枚举:
enum VideoCodec {
h264,
h265,
vp9,
}
说明
这里我刻意把 VP9 也放进来,是为了模拟真实项目的取舍:
H.264兼容性最好,默认值选它一般不会错。H.265压缩率高,但设备/播放器兼容性要评估。VP9在部分场景更省空间,但链路更复杂,通常是“可选项”而不是默认项。
DropdownButtonFormField<VideoCodec>(
value: _codec,
decoration: const InputDecoration(
labelText: '编码器',
border: OutlineInputBorder(),
),
items: VideoCodec.values
.map((e) => DropdownMenuItem(value: e, child: Text(e.name)))
.toList(),
onChanged: (value) => setState(() => _codec = value ?? _codec),
)
说明
如果你后面要做“压缩预设”(比如“省流量/均衡/高质量”),这个枚举也可以直接参与预设逻辑:预设不是单独存一套字符串,而是把 format/resolution/fps/codec 组合起来。
转换任务与进度(先把流程跑通)
示例工程里我用 Stream 模拟了转换进度,先把“能看到进度 + 能报错 + 能收尾”这条链路打通:
Stream<VideoConvertProgress> convert(VideoConvertOptions options) async* {
if (options.inputPath.trim().isEmpty) {
throw ArgumentError('inputPath is empty');
}
const stages = <String>[
'解析输入参数',
'准备输出文件',
'转码中',
'写入文件',
'收尾清理',
];
for (var i = 0; i < stages.length; i++) {
await Future<void>.delayed(const Duration(milliseconds: 450));
final percent = (i + 1) / stages.length;
yield VideoConvertProgress(percent, stages[i]);
}
}
说明
这段“模拟进度”看起来像 demo,但对真实项目很有价值:
- UI 先行:真正的转码实现通常会晚一些接入(平台侧接口、FFmpeg、权限、性能调优都要时间),先让前端链路完整,团队协作会顺。
- 错误能冒泡:输入路径为空直接
throw,调用方可以统一弹 toast/snackbar。 - 阶段文案可复用:后续接入真实进度时,也可以沿用“阶段 + 百分比”的呈现方式。
对应 UI 侧,我用弹窗展示进度条:
LinearProgressIndicator(value: percent)
说明
这里有个小细节:转换这种操作最好 不要让用户随便点空白关闭弹窗,否则你很难解释“到底在转还是没转”。示例里用的是 barrierDismissible: false,等你把取消能力做出来,再开放“取消”按钮会更合理。
总结
这一版实现的重点不是“转码引擎”,而是把真实项目里最容易被忽略的链路先做扎实:
- 入口清晰:格式转换/压缩拆成两个入口。
- 参数可控:用
enum约束格式、分辨率、编码器,减少字符串乱飞。 - 流程完整:弹窗收集参数 -> 组装
VideoConvertOptions-> 转换任务 -> 进度展示/错误提示。
后续你如果要接入真实转码,建议优先补两块:
- 文件选择与权限:不要让用户手填路径,把路径/读写权限的坑前置解决。
- 输出文件命名与落盘位置:避免覆盖源文件,同时保证用户容易找到结果文件。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)