在这里插入图片描述

视频转换功能允许用户将视频文件在不同格式之间进行转换,如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-4Mp4 这类字符串差异导致的 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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐