第3.3篇:图生视频服务——任务轮询机制实现

系列:HarmonyOS 从入门到实践 · 画伴梦工厂实战
难度:⭐⭐⭐ 高级
前置知识:3.2 火山引擎 Seedream 文生图 API 对接
涉及源文件products/default/src/main/ets/services/AIGenerationService.ets


在这里插入图片描述

一、概述

AI 视频生成与图片生成有一个本质区别:视频生成是异步任务。调用文生图 API(如第 3.2 篇的 Seedream)通常可以在一次 HTTP 请求-响应周期内返回结果,因为图片生成耗时相对较短(数秒)。但视频生成涉及数十帧图像的逐帧渲染、光流计算、拼接编码,耗时可达数分钟。若客户端保持长连接等待,不仅会耗尽连接池资源,还容易因网络波动导致请求超时断开。

"画伴梦工厂"的图生视频服务采用业界标准的 异步任务模式(Async Task Pattern)

  1. 客户端提交任务:上传图片 + Prompt,获得一个唯一 taskId
  2. 服务端异步处理:视频在服务端后台队列中生成
  3. 客户端轮询状态:以固定间隔查询任务状态,直到完成或失败
  4. 客户端下载结果:获取远程视频 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 状态码 | 是否重试 | 原因 |
|-------------|---------|

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐