HarmonyOS APP《画伴梦工厂》开发第19篇:图生视频服务——任务轮询机制实现
第3.3篇:图生视频服务——任务轮询机制实现
系列:HarmonyOS 从入门到实践 · 画伴梦工厂实战
难度:⭐⭐⭐ 高级
前置知识:3.2 火山引擎 Seedream 文生图 API 对接
涉及源文件:products/default/src/main/ets/services/AIGenerationService.ets

一、概述
AI 视频生成与图片生成有一个本质区别:视频生成是异步任务。调用文生图 API(如第 3.2 篇的 Seedream)通常可以在一次 HTTP 请求-响应周期内返回结果,因为图片生成耗时相对较短(数秒)。但视频生成涉及数十帧图像的逐帧渲染、光流计算、拼接编码,耗时可达数分钟。若客户端保持长连接等待,不仅会耗尽连接池资源,还容易因网络波动导致请求超时断开。
"画伴梦工厂"的图生视频服务采用业界标准的 异步任务模式(Async Task Pattern):
- 客户端提交任务:上传图片 + Prompt,获得一个唯一
taskId - 服务端异步处理:视频在服务端后台队列中生成
- 客户端轮询状态:以固定间隔查询任务状态,直到完成或失败
- 客户端下载结果:获取远程视频 URL,下载到本地沙箱
这一模式并非 HarmonyOS 专属,但在 ArkTS 中的实现细节——http.createHttp 的使用、setTimeout 模拟轮询间隔、onStatus 回调传递进度——值得深入拆解。
本文聚焦 AIGenerationService 类中与图生视频相关的核心方法,包括任务创建、状态轮询、超时控制、视频下载,以及它们如何与 RecognitionWaitingPage 的 UI 进度系统联动。
二、异步任务模式:generateVideo 入口
generateVideo 是整个图生视频流程的入口方法,它串联了四个步骤:
static async generateVideo(imageUrl: string, prompt: string,
onStatus?: (message: string) => void): Promise<GeneratedVideo> {
const finalPrompt = AIGenerationService.enrichPrompt(prompt);
// Step 1: 图片压缩 + Base64 编码
const uploadBase64 = await AIGenerationService.prepareUploadBase64(imageUrl, onStatus);
// Step 2: 创建视频生成任务
const task = await AIGenerationService.createImg2VideoTask(uploadBase64);
const taskId = task.taskId;
if (onStatus) { onStatus('任务已提交,正在生成动画'); }
// Step 3: 轮询等待完成
const remoteVideoUrl = await AIGenerationService.pollImg2VideoTask(taskId, onStatus);
if (onStatus) { onStatus('动画已生成,正在保存到本地'); }
// Step 4: 下载视频到本地
const localUri = await AIGenerationService.downloadVideo(remoteVideoUrl, taskId);
return { prompt: finalPrompt, videoUri: localUri, taskId: taskId, remoteVideoUrl: remoteVideoUrl };
}
设计要点:
| 方面 | 说明 |
|---|---|
onStatus 回调 |
可选的回调函数,用于将服务端处理进度实时传递给 UI 层。页面调用时传入 (message) => { this.statusText = message; },驱动 UI 文本更新 |
| 四步串行 | 每一步 await 等待上一步完成,天然形成流程控制。若中间某步抛出异常,整个 Promise 拒绝,由调用方的 try-catch 统一处理 |
| 错误传播 | 内部方法不吞异常,直接向上抛出。调用方(RecognitionWaitingPage.startGeneration)捕获后设置 failed = true |
返回的 GeneratedVideo 接口包含四个字段:
export interface GeneratedVideo {
prompt: string; // 增强后的 Prompt
videoUri: string; // 本地沙箱路径 (file://...)
taskId: string; // 服务端任务 ID
remoteVideoUrl: string; // 远端视频地址
}
三、任务提交:createImg2VideoTask
第一步是将经过压缩和 Base64 编码的图片数据提交到图生视频服务端,获取一个唯一的 taskId。
private static async createImg2VideoTask(base64: string): Promise<GeneratedVideoTask> {
const request = http.createHttp();
try {
const headers = { 'Content-Type': 'application/json' };
const body: Img2VideoCreateRequest = { base64: base64 };
const response = await request.request(IMG2VIDEO_CREATE_URL, {
method: http.RequestMethod.POST,
expectDataType: http.HttpDataType.STRING,
connectTimeout: 30000,
readTimeout: 180000, // 3 分钟超时
header: headers,
extraData: JSON.stringify(body)
});
const responseText = response.result.toString();
const responseBody = JSON.parse(responseText) as Img2VideoCreateResponse;
const taskId = AIGenerationService.pickTaskId(responseBody);
return { taskId: taskId, imageUrl: responseBody.imgUrl ? responseBody.imgUrl : '' };
} finally {
request.destroy();
}
}
3.1 请求配置分析
connectTimeout: 30000:连接超时 30 秒。若服务端不可达,快速失败而非长期等待。readTimeout: 180000:读取超时 3 分钟。考虑到上传 Base64 数据可能较大(压缩后约 1.2MB 的文本),服务端解码和处理需要时间,给予充足的超时窗口。expectDataType: STRING:期望响应以字符串形式返回,便于直接JSON.parse。
3.2 taskId 字段兼容处理
不同版本的服务端接口可能返回不同大小写的字段名,pickTaskId 做了兼容:
private static pickTaskId(responseBody: Img2VideoCreateResponse): string {
if (responseBody.taskid && responseBody.taskid !== '') {
return responseBody.taskid; // 小写格式
}
if (responseBody.taskId && responseBody.taskId !== '') {
return responseBody.taskId; // 驼峰格式
}
return ''; // 未找到,后续调用方会抛异常
}
这种防御式编程在对接第三方服务时尤为重要——服务端接口字段命名变化不应导致客户端崩溃。
四、轮询机制:pollImg2VideoTask
获得 taskId 后,客户端进入核心的轮询循环。这是整个图生视频最复杂、最关键的逻辑。
private static async pollImg2VideoTask(taskId: string,
onStatus?: (message: string) => void): Promise<string> {
const startedAt = Date.now();
let attempt = 0;
let lastErrorMessage = '';
while (Date.now() - startedAt < MAX_SEEDANCE_WAIT_MS) {
attempt++;
if (onStatus) {
onStatus('正在等待动画生成,第 ' + attempt.toString() + ' 次检查');
}
let responseBody: Img2VideoStatusResponse;
try {
responseBody = await AIGenerationService.queryImg2VideoTask(taskId);
} catch (error) {
lastErrorMessage = AIGenerationService.getErrorMessage(error as Error);
if (!AIGenerationService.canRetryTaskQuery(lastErrorMessage)) {
throw new Error(lastErrorMessage);
}
if (onStatus) { onStatus('网络有点慢,继续等待动画完成'); }
await AIGenerationService.sleep(POLL_INTERVAL_MS);
continue;
}
const videoUrl = AIGenerationService.pickImg2VideoUrl(responseBody);
if (responseBody.status === 1 && videoUrl !== '') {
return videoUrl;
}
if (responseBody.status !== undefined && responseBody.status < 0) {
throw new Error(responseBody.message ? responseBody.message : '视频生成任务失败');
}
await AIGenerationService.sleep(POLL_INTERVAL_MS);
}
throw new Error('视频生成超时,请稍后重试');
}
4.1 轮询参数
| 常量 | 值 | 说明 |
|---|---|---|
POLL_INTERVAL_MS |
5000 (5s) | 每次轮询的时间间隔 |
MAX_SEEDANCE_WAIT_MS |
360000 (6min) | 最大等待时间,超过则判定为超时 |
轮询间隔 5 秒是实践中的平衡值——太短会增加服务端压力,太长则让用户等待反馈变得迟钝。
4.2 轮询状态机
┌─────────────┐
│ 查询任务状态 │
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
网络异常 status=1 status<0
│ + videoUrl │
▼ │ ▼
canRetry? ───no──→ 抛出异常 抛出异常
│yes
▼
等待 5s ──→ 继续轮询
4.3 状态码映射
服务端返回的 status 字段含义:
| status 值 | 含义 | 客户端处理 |
|---|---|---|
1 |
生成完成 | 检查 url/sdUrl 字段是否有值,有则返回视频 URL |
0 或正数(非 1) |
正在生成中 | 继续等待,5 秒后再次查询 |
-1 及以下负数 |
生成失败 | 抛出异常,携带 message 字段中的错误描述 |
undefined |
未知状态 | 继续等待(兜底保守策略) |
关键判断逻辑:
// 完成条件:status === 1 且 videoUrl 不为空
if (responseBody.status === 1 && videoUrl !== '') {
return videoUrl;
}
// 失败条件:status 为负数
if (responseBody.status !== undefined && responseBody.status < 0) {
throw new Error(responseBody.message ? responseBody.message : '视频生成任务失败');
}
4.4 URL 字段兼容
与 pickTaskId 类似,pickImg2VideoUrl 也对不同字段名做了兼容:
private static pickImg2VideoUrl(responseBody: Img2VideoStatusResponse): string {
if (responseBody.url && responseBody.url !== '') {
return responseBody.url;
}
if (responseBody.sdUrl && responseBody.sdUrl !== '') {
return responseBody.sdUrl;
}
return '';
}
五、超时控制与重试逻辑
5.1 硬超时(Hard Timeout)
轮询循环使用 Date.now() 计算已耗时:
const startedAt = Date.now();
while (Date.now() - startedAt < MAX_SEEDANCE_WAIT_MS) {
// ... 轮询逻辑
}
throw new Error('视频生成超时,请稍后重试');
6 分钟是最长等待时间。如果超过这个时间服务端仍未完成,客户端不再继续等待。超时异常中还会附带最近一次网络错误的信息(lastErrorMessage),帮助排查问题。
5.2 网络错误重试
当 queryImg2VideoTask 抛出异常时(网络断开、DNS 解析失败、HTTP 5xx 等),pollImg2VideoTask 并不会立即放弃,而是判断是否可以重试:
private static canRetryTaskQuery(message: string): boolean {
return message.indexOf('400') < 0 && message.indexOf('401') < 0 &&
message.indexOf('403') < 0 && message.indexOf('404') < 0;
}
重试策略:
| HTTP 状态码 | 是否重试 | 原因 |
|-------------|---------|
更多推荐


所有评论(0)