前台服务与任务队列
架构一图流
添加文件 → 逐项设置 → 待开始队列 → 前台服务 → 引擎 → 保存输出
这个架构在 README 里有完整描述,是理解 ZenConverter 为什么"不假装自己是万能转换器"的关键——UI 不直接做转换,每个任务按输入/输出/模式选择引擎。
为什么用前台服务
Android 系统对后台进程的限制越来越严:
- 普通 Service 在应用切到后台后很容易被系统杀掉;
- WorkManager 适合短任务,不适合几 GB 视频的长时间转换(可能跑几十分钟甚至几小时);
- 只有 Foreground Service 能在系统任务列表里显示一个常驻通知,系统会把它当作"用户明确想继续运行的任务"而保持存活。
ZenConverter 在以下两种场景用前台服务:
- 转换任务:任何格式转换都走前台服务;
- 模型下载:AI 模型和 Noto CJK 字体包下载也走前台服务(防止下载到一半被系统杀掉)。
任务队列的核心概念
| 概念 | 说明 |
|---|---|
ConversionRequest | 一个转换任务的完整定义(输入文件、输出格式、选项、引擎) |
ConversionEvent | 任务的状态事件(开始/进度/完成/失败/取消) |
StateFlow | Kotlin 协程里的响应式状态流,UI 订阅它来更新显示 |
| 任务注册表 | 管理所有活跃任务,记录状态 |
每个文件一个任务实例。选 10 个文件 = 10 个任务实例,每个独立运行、独立显示进度、可以单独取消。
任务的可观测性
每个任务会发布以下信息到前台服务通知和应用内的任务列表:
| 信息 | 说明 |
|---|---|
| 任务名称 | 源文件名 + 输出格式 |
| 当前状态 | 等待中 / 运行中 / 已完成 / 失败 / 已取消 |
| 进度百分比 | 从 0% 到 100% |
| 当前处理步骤 | 解码 → 滤镜 → 编码 → 保存 |
| 失败原因 | 明确的错误信息("Vulkan 不可用""FFmpeg 缺 wmav2 编码器""密码错误") |
失败时不会静默——每个失败的任务都会在任务列表里显示红色状态 + 错误说明。UI 不隐藏任何失败。
任务的可取消性
取消是即时生效的,不等待当前操作跑完:
- 正在编码的任务会停止编码;
- 正在解码的任务会停止解码;
- 已完成的中间产物(比如下载了一半的文件、生成了一半的临时文件)会被清理。
取消操作通过协程的 Job.cancel() 实现——引擎内部的解码/编码循环每次迭代都会检查 isActive,及时响应取消信号。
输出处理
任务完成后有三种处理:
- 通知:前台服务通知点击跳回应用;
- 分享:长按任务 → 分享输出文件(Android 系统分享面板);
- 打开:应用会"尽力"调用系统 Intent 打开输出文件或所在文件夹(不保证成功,取决于设备上有没有能打开这个文件类型的应用)。
v0.2.3 的 ACTION_GET_CONTENT 改进
v0.2.3 的 feat(import): prefer ACTION_GET_CONTENT for broad file provider support commit 改进了导入路径(见 SAF 文件访问页面)。这个改进让云盘、SMB、第三方文件管理器的文件都能正确进入任务队列。
已知边界
- 多任务并行还是串行:ZenConverter 当前按选择顺序逐个串行处理任务(不是并行)。这是故意的——每个引擎的内存和 CPU 占用很高,并行跑多个大视频转换会触发 OOM;
- 临时文件清理:某些引擎(尤其是 FFmpeg)在转换过程中可能产生临时文件。正常完成、失败、取消后都会清理;
- 通知权限:Android 13+(API 33+)需要动态请求 POST_NOTIFICATIONS 权限才能显示前台服务通知。
相关页面
- SAF 文件访问与导入路径 —— 任务从哪里来
- 视频格式互转原理 —— 任务到引擎的路由
- 动态像素预算与 OOM 防护 —— 为什么串行而不是并行
