
不接视频服务器:怎样在 HarmonyOS 手机上把 3 张照片生成 6 秒回忆电影
真机环境:ALN-AL80,OpenHarmony 7.0.0.32(Beta2),API 26;3 张输入图均为公开测试素材,编码、配乐合成、播放和保存全部在手机本地完成。
为什么相册里的照片需要“成片”出口
旅行结束后,相册里通常留下几十张照片。逐张翻看保留了细节,但分享时缺少节奏;把照片上传到服务器生成视频,又会增加等待、流量和隐私成本。
双镜记忆相机需要一条更短的路径:选中几张照片,确定时长与配乐,在本机生成可播放的 MP4。用户可以先预览,确认无误后再存入成片列表。网络中断不影响编码,照片也不需要离开设备。
本次真机实践选择 3 张公开照片,设置 6 秒时长和“晨光微风”配乐,生成 720×1280 的竖屏 AVC 视频。取证覆盖选片、配置、生成、播放和保存,不把静态设计稿当成视频结果。
本地生成要解决哪些问题
HarmonyOS 官方文档中心把视频编解码归入 AVCodec Kit,把 AVPlayer 播放归入 Media Kit。前者负责把连续图像帧编码为视频,后者负责生成结果的本地播放与状态控制。
照片生成短片不是把 JPEG 文件简单拼接在一起,至少包含五段处理:
选择照片
→ 按时长分配帧
→ 裁剪、缩放并转换为编码像素格式
→ AVC 编码为 MP4
→ 合并配乐并交给播放器预览
输入照片横竖比例不同,编码器又要求稳定的宽高、偶数尺寸和可接受的像素格式。任何一步处理不一致,最终都可能表现为花屏、拉伸、编码失败或只有声音没有画面。
第一步:选择参与成片的照片
页面先展示当前公开相册,私密记录不会进入候选列表。普通短片至少需要两张照片;本次依次选择旅行合成图、人物原图和城市夜景。

图 1:真机进入普通短片选片页,公开照片以缩略图形式展示。
选片结果保存的是相册记录对应的真实文件路径。开始生成前再次过滤空路径,防止照片在页面停留期间被删除:
const normalizedPaths = sourceImagePaths.filter(
path => path.trim().length > 0
);
if (normalizedPaths.length < 2) {
throw new Error('至少需要 2 张素材才能生成普通短片');
}
第二步:把“6 秒”换算成真实视频帧
生成器使用 2 fps。6 秒短片需要 12 帧,三张照片按时间区间均匀分配,每张图连续占用四帧。这里的低帧率适合静态照片轮播,也能控制短片体积;如果加入平移和缩放动画,帧率要相应提高。
const frameRate = 2;
const targetFrameCount = Math.round(durationSeconds * frameRate);
const framePaths: Array<string> = [];
for (let index = 0; index < targetFrameCount; index++) {
const sourceIndex = Math.min(
normalizedPaths.length – 1,
Math.floor(index * normalizedPaths.length / targetFrameCount)
);
framePaths.push(normalizedPaths[sourceIndex]);
}
时长被限制在 3~30 秒,防止误操作生成过长任务。生成按钮按下以后,页面冻结当前选片和参数,避免编码中途修改素材导致帧序列与 UI 不一致。
本次配置为手动选片、6 秒、“晨光微风”配乐,并在生成请求中打开智能降噪、空间音频、淡入淡出和宽阔声场。

图 2:页面显示已选 3 张,时长、配乐和音频处理选项均已确定。
截图能够证明这些参数进入了本次生成请求。降噪和空间音频的实际听感需要耳机与音频分析验证,不能仅凭开关状态下结论;本次结果确认的是配乐音轨已经写入并可随视频播放。
第三步:不同尺寸照片统一为 720×1280
直接把横图缩放到竖屏会把人物和建筑拉长。生成器采用居中 cover 裁剪:先根据源图与目标画布的宽高比计算裁剪区域,再缩放到 720×1280。裁剪宽高和偏移都落在偶数像素边界,满足 NV12 编码要求。
const cropRegion = MovieFramePolicy.buildCoverCropRegion(
{ width: imageInfo.size.width, height: imageInfo.size.height },
{ width: 720, height: 1280 }
);
真机接入时遇到的关键问题发生在解码阶段:API 26 上,JPEG 解码同时请求“裁剪 + 缩放 + NV12”会失败。稳定路径是先按文档支持良好的 RGBA_8888 完成裁剪和缩放,再把 PixelMap 转成 NV12。
const pixelMap = await imageSource.createPixelMap({
editable: true,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
desiredSize: { width: 720, height: 1280 },
desiredRegion: cropRegion,
cropAndScaleStrategy: image.CropAndScaleStrategy.CROP_FIRST
});
await pixelMap.convertPixelFormat(image.PixelMapFormat.NV12);
这个顺序多了一次像素格式转换,却把 JPEG 解码和视频编码的职责分开,错误也更容易定位。全部帧进入编码队列后,PixelMap 会集中释放,避免 12 帧同时长期占用图像内存。
第四步:编码 AVC 并合并本地配乐
当前输出规格为 720×1280、2 fps、12 Mbps、video/avc。ArkTS 把准备好的 NV12 PixelMap 推给本地原生编码层,再传入输出文件描述符、画面尺寸、帧率和帧数。
transcoding.startTransCode(
outputFd,
720,
1280,
'video/avc',
2,
12 * 1024 * 1024,
frameCount,
onFinished
);
编码先写入临时无声 MP4。选择配乐后,本地合成层把应用资源中的 M4A 音轨写入最终文件;合并成功才删除临时文件。自定义音乐无法写入时直接提示格式问题,内置配乐失败则保留可播放的无声版本,避免整段视频一起丢失。
const result = await transcoding.mergeMovieAudioEnhanced(
stagedVideoPath,
musicPath,
finalVideoPath,
durationUs,
noiseReduction,
spatialAudio,
soundFieldType,
fadeEnabled ? 0.8 : 0,
fadeEnabled ? 1.2 : 0
);
第五步:用真实播放器完成结果闭环
编码结束后,页面进入新生成的短片预览。画面显示 6 秒时长、三张素材缩略图和“晨光微风”配乐,底部“存入成片”按钮此时才可用。

图 3:真机生成结果页显示 00:06、3 张照片和配乐名称,视频文件已经可以预览。
预览音频由 AVPlayer 读取本地文件描述符。播放器设置为音乐用途,准备完成后响应播放、暂停和页面离开时的释放:
const file = fs.openSync(videoPath, fs.OpenMode.READ_ONLY);
player.fdSrc = {
fd: file.fd,
offset: 0,
length: fs.statSync(videoPath).size
};
await preparePlayer(player); // 将 callback 形式的 prepare 封装为 Promise
await player.play();

图 4:播放进度已经从 00:00 前进,说明页面展示的是可播放 MP4,而不是一张静态封面。
确认预览后点击“存入成片”,视频记录写入本地成片列表。列表从 0 条变为 1 条,并显示封面、00:06、三张照片和配乐名称。

图 5:短片已经保存,列表状态为“已保存”,完整链路到此结束。
这条链路为什么没有接服务器
照片解码、画幅处理、视频编码、音轨合并和播放都使用本地文件。网络不参与生成过程,公开测试照片没有上传。带来的直接收益有三点:
- 选择照片后可以立刻生成,失败原因集中在本地格式和资源状态;
- 私密照片无需跨越网络边界,但业务仍要在选片入口过滤访问权限;
- 生成文件可以直接进入应用的本地成片列表,再由用户决定是否分享。
本地处理也要控制资源。720×1280 的多帧 PixelMap 会快速占用内存,长视频不能一次把全部帧常驻内存;当前 3~30 秒的照片短片适合批量准备,继续扩展时应改成生产者—消费者式逐帧送入编码器。
最终效果
这次真机实践完成了可复查的完整结果:三张公开照片被选中,按 6 秒和 2 fps 生成 12 帧,统一裁剪为 720×1280,转换为 NV12 后编码成 AVC,随后写入“晨光微风”配乐。生成结果能够在真机播放,进度正常前进,并成功保存到成片列表。
最关键的工程修正是把 JPEG 的 RGBA 解码与 NV12 编码输入拆成两步。解决这个格式问题后,照片成片不再依赖云端接口,用户从选片到得到可播放 MP4 只需要一次本地流程。
网硕互联帮助中心






评论前必须登录!
注册