一、从方案稿升级为有运行结果的实现复盘

动图魔方的视频转 GIF 已经不是接口规划。项目内存在真实的 VideoFrameExtractorFrameProcessorExportService,模拟器中也完成了视频缩略帧预览、裁剪、抽帧、GIF 合成、相册授权和作品列表回读。

短片段真实导出结果

本次使用项目内置的 CC0 花朵视频作为可重复素材。0.56 秒、15fps、标清参数完成导出,页面回读为 480×270 · 8帧 · 15fps · 1.1MB;第二轮 0.61 秒片段回读为 9 帧。长片段失败边界也被保留,后文单独说明。

二、实现入口按媒体类型进入不同管线

导出服务根据素材类型分派任务。视频分支调用 buildFromVideo(),而不是把视频当普通图片序列处理。

static async build(
  preset: ExportPreset,
  signal: ExportSignal
): Promise<GifBuildOutput> {
  if (preset.sourceType === 'video') {
    return await ExportService.buildFromVideo(preset, signal)
  }
  return await ExportService.buildFromImages(preset, signal)
}

编辑器把裁剪起点、选中时长、帧率、画质和输出比例归一化后传入服务。标清对应较小长边,能够降低每帧 PixelMap 的内存与编码压力。

参数 本次成功值 运行结果
视频片段 0.56 秒 成功
帧率 15fps 8 帧
画质 标清 480×270
输出 GIF + 相册保存 1.1MB

三、先用探测帧确定真实尺寸

VideoFrameExtractor.extract() 创建 AVImageGenerator,先读取一张探测帧取得宽高,再按目标长边计算 width × height。探测帧完成后立即释放。

const probeFrame = await generator.fetchFrameByTime(
  Math.round(startSec * 1_000_000),
  queryOption,
  {}
)
const info = await probeFrame.getImageInfo()
await probeFrame.release()

运行日志中出现连续的 CreatePixelMap success,并记录预览帧数量和目标尺寸,证明这里不是只检查了静态源码。预览阶段实际生成了 120 帧缓存并开始播放,编辑器时间轴能够显示花朵视频缩略图。

四、采样点使用微秒时间戳逐帧获取

正式抽帧循环根据选中区间和目标帧数计算时间点,并通过 fetchFrameByTime() 获取 PixelMap。每次获取后报告阶段进度,取消信号会阻止继续请求。

for (let index = 0; index < frameCount; index++) {
  signal.throwIfCancelled()
  const ratio = frameCount <= 1 ? 0 : index / frameCount
  const timeUs = Math.round((startSec + durationSec * ratio) * 1_000_000)
  const frame = await generator.fetchFrameByTime(
    timeUs,
    queryOption,
    frameParams
  )
  frames.push(frame)
  signal.reportProgress('extract_video_frame', index + 1, frameCount)
}

模拟器故障日志还记录了 75 帧任务的抽取进度,从 26/75、30/75 一直推进到 64/75,并伴随 PixelMap 创建记录。这说明长任务失败前确实进入了真实抽帧链路。

五、抽出的 PixelMap 进入处理与 GIF 编码

ExportService.buildFromVideo() 把视频帧交给帧处理器,按比例、画质、滤镜与字幕参数转换为编码帧,再进入 GIF 编码器。成功后把输出文件登记为作品,并请求保存到系统相册。

const pixelMaps = await VideoFrameExtractor.extract(
  preset.sourceUris[0],
  selectedDuration,
  fps,
  signal,
  { startSec, targetMaxEdge }
)
try {
  const frames = await FrameProcessor.fromPixelMaps(pixelMaps, preset, signal)
  return await GifEncoder.encode(frames, preset, signal)
} finally {
  await ExportService.releasePixelMaps(pixelMaps)
}

系统第一次保存会弹出“允许动图魔方保存 1 张图片?”的确认。授权后页面回读 100% 和最终尺寸,作品页出现 视频转GIF_1;第二次完成后又出现 视频转GIF_2

六、真实失败边界:长片段会阻塞主线程

5.1 秒、15fps、标清预计 75 帧。模拟器运行时任务持续抽帧,但系统最终生成 THREAD_BLOCK_6S 故障日志,明确写出 App main thread is not response!,应用随后退回桌面。

这意味着当前实现只能证明短片段导出闭环,不能声称长片段已经稳定。失败不是 PixelMap 接口“不存在”,而是重任务仍占用主线程过久。下一步需要:

  1. 抽帧与编码迁移到 TaskPool/Worker 或原生异步任务。
  2. 分批处理帧并主动让出主线程。
  3. 把安全帧预算与设备性能关联,不只使用固定上限。
  4. 增加超时、取消和中间资源释放验证。

七、如何复现本次结果

  1. 选择视频并进入“视频转 GIF”。
  2. 把范围裁到约 0.6 秒。
  3. 画布选择 15fps、标清。
  4. 点击导出,允许系统保存图片。
  5. 回读最终尺寸、帧数和作品列表。

关于 PixelMap 的平台操作可参考华为 HarmonyOS 图像开发指导

八、总结

动图魔方已实现从 AVImageGenerator 视频抽帧、PixelMap 处理、GIF 编码到相册与作品记录的真实链路。短片段两次导出成功足以把文章从方案稿升级为实战,但 75 帧长任务触发主线程阻塞也必须同时公开,这才是完整、可复现的工程结论。

Logo

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

更多推荐