动图魔方 HarmonyOS 实战续篇(21):视频抽帧到 PixelMap 的真实导出管线
一、从方案稿升级为有运行结果的实现复盘
动图魔方的视频转 GIF 已经不是接口规划。项目内存在真实的 VideoFrameExtractor、FrameProcessor 与 ExportService,模拟器中也完成了视频缩略帧预览、裁剪、抽帧、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 接口“不存在”,而是重任务仍占用主线程过久。下一步需要:
- 抽帧与编码迁移到 TaskPool/Worker 或原生异步任务。
- 分批处理帧并主动让出主线程。
- 把安全帧预算与设备性能关联,不只使用固定上限。
- 增加超时、取消和中间资源释放验证。
七、如何复现本次结果
- 选择视频并进入“视频转 GIF”。
- 把范围裁到约 0.6 秒。
- 画布选择 15fps、标清。
- 点击导出,允许系统保存图片。
- 回读最终尺寸、帧数和作品列表。
关于 PixelMap 的平台操作可参考华为 HarmonyOS 图像开发指导。
八、总结
动图魔方已实现从 AVImageGenerator 视频抽帧、PixelMap 处理、GIF 编码到相册与作品记录的真实链路。短片段两次导出成功足以把文章从方案稿升级为实战,但 75 帧长任务触发主线程阻塞也必须同时公开,这才是完整、可复现的工程结论。
更多推荐



所有评论(0)