面向多项目复用的开源跨端动态照片基础库,目标平台为 Android、iOS 和 HarmonyOS。
项目采用“统一传输格式 + 平台原生播放器”的架构:发送端把不同系统相册格式导出为 Portable Motion Photo。宿主注入单资源 Loader 复用已有鉴权、下载和缓存能力;MotionPhotoKit 负责调度两份资源、格式规范化及平台播放准备。宿主始终拥有下载缓存,播放器不判断素材来自 Apple、Android 还是鸿蒙。
点击流程图可打开 SVG 大图并缩放查看。可编辑源文件见 motionphotokit-flow.mmd。
宿主最核心的接入点是 MotionPhotoResourceLoader:继续复用项目已有的网络、鉴权、下载和缓存设施,
每次只把一份资源转换为可读取的本地资源。共享库负责图片与视频的成对调度、格式统一、checksum 计算、
平台播放准备和原生 View;它不会申请业务权限、上传文件、接管宿主缓存或决定缓存淘汰策略。
如果业务需要发送动态照片,宿主还需要上传 Exporter 生成的 JPEG 和 MP4,并把两个 URL 与 Manifest
提交给服务端。接收端不再关心动态照片最初来自哪个平台:宿主只把 Manifest JSON、JPEG URI 和 MP4 URI
交给 PortableMotionPhotoFactory,由共享库解析 Manifest、补齐资源元数据并构造 PortableMotionPhoto。
三端都把动态照片拆成一张 JPEG、一段 H.264 MP4 和一份只包含元数据的 Manifest。Manifest 默认只存在于
内存或业务请求中,MotionPhotoKit 不会额外创建 manifest.json。媒体资源有两条主要流向:
发送端
系统相册 / 本地原始资源对
│
▼
平台 Exporter / HarmonyOS Importer
方向烘焙、转码、checksum、revision
│
▼
Portable Motion Photo v1
still.jpg + motion.mp4 + Manifest
│
├── 宿主上传媒体并发送 Manifest
└── 上传或保存结束后归还租约,删除库生成的临时文件
接收端
Manifest + 图片资源标识 + 视频资源标识
│
▼
PortableMotionPhotoFactory
│
▼
ResourceCoordinator(合并相同请求、限制并发、传播取消)
│
▼
宿主 MotionPhotoResourceLoader
下载 / 鉴权 / 解密 / 校验 / 缓存
│
▼
本地 JPEG + 本地 MP4(所有权仍属于宿主)
│
├── Android:Bitmap 封面 + ExoPlayer 直接读取 MP4
├── iOS:生成 Apple paired JPEG/MOV → PHLivePhotoView
└── HarmonyOS:loadMovingPhoto → MovingPhotoView
ResourceCoordinator 和 Materializer 只调度 URI,不会把宿主 Loader 返回的文件复制到库目录,也不会在
View 释放时删除它们。宿主必须保证文件在播放、上传或保存期间有效,并自行决定缓存路径和淘汰策略。
下表中的 <android-cache> 和 <harmony-cache> 分别表示对应应用上下文的 cacheDir;<apple-tmp>
表示 NSTemporaryDirectory()。UUID、时间戳和序号每次运行都会变化。
| 用途 | Android | iOS | HarmonyOS | 所有者与清理方式 |
|---|---|---|---|---|
| 宿主 Loader 本地资源 | 路径由宿主决定,通常是 file:// 或可读的 content:// |
路径由宿主决定,播放准备前必须可解析为本地文件 URL | 路径由宿主决定,HAR 要求 Loader 返回 file:// |
始终归宿主;共享库只取消请求,不删除文件 |
| 发送端 Portable 输出 | <android-cache>/motionphotokit/export-<UUID>/ |
<apple-tmp>/motionphotokit/export-<UUID>/ |
<harmony-cache>/motionphotokit-import-<timestamp>-<seq>/ |
Android/iOS 使用 ExportedPortableMotionPhoto.deleteTemporaryFiles();鸿蒙使用 HarmonyImportedPortableMotionPhoto.deleteTemporaryFiles() |
| 播放中间产物 | 无磁盘中间产物 | <apple-tmp>/motionphotokit/playback-<UUID>/ |
无库自有磁盘中间产物 | 只有 iOS 需要配对资源租约和 Kit 级缓存 |
| 保存到系统相册的临时输入 | <android-cache>/motionphotokit/export-<UUID>/,调用结束自动删除 |
复用 playback-* 配对资源,PhotoKit 完成后释放引用 |
直接把 Loader/Importer 的本地 URI 交给系统 API | 系统相册最终资产不再属于共享库,库不会删除 |
鸿蒙 Demo 的 Loader 把网络测试素材放在
<harmony-cache>/motionphotokit-demo-resources/<role>-<url-hash>.<ext>。这是 harmonyApp 的宿主缓存示例,
不是 HAR 规定的目录;正式业务可以使用自己的下载缓存。
AndroidMotionPhotoKit.export() 会创建:
<android-cache>/motionphotokit/export-<UUID>/
├── source-motion-photo.jpg # 系统相册返回的 JPEG 临时副本,按来源出现
├── source-still.jpg # 从单文件容器拆出的原始静态图,按来源出现
├── source-motion.mp4 # 从单文件拆出或从双文件相册读取的原始视频,按来源出现
├── source-still # 原始资源对的临时副本,按来源出现
├── source-motion # 原始资源对的临时副本,按来源出现
├── still.jpg # 最终 Portable JPEG
└── motion.mp4 # 最终 Portable H.264 MP4
调用成功后,ExportedPortableMotionPhoto.value 中的 URI 指向最终 still.jpg 和 motion.mp4。宿主必须
持有返回的租约直到图片、视频上传以及 Manifest 发送全部结束:
val exported = motionPhotoKit.export(source)
try {
upload(exported.value)
} finally {
exported.deleteTemporaryFiles()
}失败或协程取消时 Exporter 会立即删除整个目录。App 崩溃或进程被终止时来不及归还的目录,可在下次 冷启动、创建共享 Kit 前清理:
AndroidMotionPhotoTemporaryStorage.cleanupOrphanedExports(applicationContext)清理器只处理不属于当前进程活跃租约的 export-*,不会触碰宿主 Loader/Downloader 文件。
系统相册导入时,库先用 Media3 解析 JPEG + appended MP4 标准单文件。若系统只向 Photo Picker 暴露
JPEG,库再通过公开 MediaStore 字段查找同目录、同名 stem 的 MP4(例如 IMG_001.jpg 与
IMG_001.mp4)。在“仅限所选照片”模式下,如果普通 MediaStore 查询隐藏了伴生视频,但相册已同时授予
图片和视频 Picker URI,库会按能力探测相册提供的 live_photo 关联键来定位已授权视频;不按厂商品牌
分支,也不依赖原始文件路径。宿主仍需确保系统选择结果包含或授权伴生视频。若厂商使用 Media3 无法
识别的私有单文件,当前实现会明确失败,不再使用已删除的 AndroidMotionPhotoContainer 扫描任意
ftyp;应基于真实样本增加独立、可测试的格式适配器。
Android Materializer 从宿主 Loader 取得本地 JPEG/MP4 后不再创建文件。Preparer 只把 JPEG 解码成最大边
不超过 2048 像素的内存 Bitmap,并把原 MP4 URI 交给 ExoPlayer。View reset()/dispose() 或离屏时会
释放 ExoPlayer;替换封面或销毁 View 时会回收 Bitmap。宿主本地 JPEG/MP4 仍由宿主缓存管理。
saveToPhotoLibrary() 会在同一个 Android 临时根目录创建一份短生命周期工作区:
<android-cache>/motionphotokit/export-<UUID>/
├── still.jpeg
└── motion.mp4
库使用 Media3 生成标准 JPEG + XMP + appended MP4 单文件 Motion Photo。
无论保存成功还是失败,工作目录都会在 finally 中删除;若进程在保存期间被直接终止,下次冷启动的
cleanupOrphanedExports() 会处理残留目录。
AppleMotionPhotoKit.export() 使用系统临时目录:
<apple-tmp>/motionphotokit/export-<UUID>/
├── source-still # 从 PHAsset 读取的图片,系统相册来源时出现
├── source-motion.mov # 从 PHAsset 读取的 paired video,系统相册来源时出现
├── still.jpg # 最终 Portable JPEG
└── motion.mp4 # 最终 Portable H.264 MP4
生命周期和 Android 相同:上传或保存结束后调用
ExportedPortableMotionPhoto.deleteTemporaryFiles();失败和取消会立即清理本次目录。
PHLivePhotoView 不能直接把普通 JPEG + MP4 当作 Live Photo 播放。Apple Preparer 会为同一份
Portable 写入一致的 Asset Identifier 和 still-image-time metadata,并生成:
<apple-tmp>/motionphotokit/playback-<UUID>/
├── paired-still.jpg
└── paired-motion.mov
这些文件由 ApplePreparedAssetCoordinator 按 Manifest revision 合并准备任务并缓存,默认最多缓存
16 份;正在被 View 或系统相册保存流程使用的资源通过引用计数保护。调用
AppleMotionPhotoKit.clearPreparedCache() 会立即删除未被使用的缓存,仍在使用的目录会等最后一个引用
释放后删除。
异常退出遗留目录可在下次冷启动清理:
AppleMotionPhotoTemporaryStorage.shared.cleanupOrphanedExports()
AppleMotionPhotoTemporaryStorage.shared.cleanupOrphanedPlaybackResources()当前进程仍在导出、播放或由缓存持有的目录会被跳过。
saveToPhotoLibrary() 与播放共用 Apple 配对准备流程。下载和配对阶段可以取消;PhotoKit 提交开始后,
库会等待系统写入完成再释放 paired 文件引用,避免导入过程中临时文件被提前删除。是否继续保留在
Prepared Cache 中由缓存容量和 clearPreparedCache() 决定。
鸿蒙当前由 ArkTS MotionPhotoSystemAssetImporter 完成系统 Moving Photo 到 Portable v1 的转换:
<harmony-cache>/motionphotokit-import-<timestamp>-<seq>/
├── source-image # 系统 Moving Photo 原始图片,规范化完成后立即删除
├── source-video # 系统 Moving Photo 原始视频,规范化完成后立即删除
├── still.jpg # 最终 Portable JPEG
└── motion.mp4 # 最终 Portable H.264 MP4
成功返回前会先删除 source-image 和 source-video,HarmonyImportedPortableMotionPhoto.source 只引用
最终两份 Portable 文件。上传、播放或保存完成后调用:
imported.deleteTemporaryFiles()失败时 Importer 会删除本次创建的全部文件和目录。当前 HAR 没有 Android/iOS 对应的孤儿目录扫描 API;
进程异常终止后的残留文件位于应用 cacheDir,由系统缓存回收策略处理。正常流程仍必须显式归还每个
Importer 结果。
HarmonyMotionPhotoKit 并发本地化 JPEG/MP4 后不创建新的文件。Preparer 直接调用
MediaAssetManager.loadMovingPhoto(context, stillUri, motionUri) 得到系统 MovingPhoto 对象,
MovingPhotoView 使用结束后只释放对象和控制器绑定,不删除宿主文件。
HarmonyMotionPhotoLibrarySaver.save() 同样不创建库自有工作目录,而是把已经位于应用沙箱的
file:// JPEG/MP4 直接作为 IMAGE/VIDEO resource 提交给系统相册。宿主或 Importer 必须等
save() 完成后才能删除输入文件。
- 宿主 Loader 文件归宿主。 View、Kit、取消请求和临时目录清理器都不会删除;宿主至少保留到最后 一个播放、上传或保存消费者结束。
- Exporter/Importer 文件归返回租约。 Android/iOS 调用
deleteTemporaryFiles(),鸿蒙调用HarmonyImportedPortableMotionPhoto.deleteTemporaryFiles();调用后 Portable 中的本地 URI 立即失效。 - Apple 配对文件归 Prepared Cache/引用。 View 不直接删除目录,而是在解绑时释放引用;缓存淘汰或
clearPreparedCache()决定最终删除时机。 - 播放器内存和系统句柄归 View。 Android 回收 Bitmap/ExoPlayer,iOS 释放
PHLivePhotoView绑定, 鸿蒙释放MovingPhoto与系统控制器;它们都不拥有宿主下载文件。 - 系统相册最终资产归系统和用户。
saveToPhotoLibrary/save成功后,Kit 不会删除或覆盖相册资产; 只清理为了本次操作创建的临时输入。
主要是为了让宿主接入更轻量。不同宿主项目不一定都在使用 Compose,如果基础库采用 Compose UI,所有 宿主都需要额外引入 Compose Runtime 和相关依赖。
因此 KMP 只负责共享数据模型和处理逻辑,播放器继续使用各平台原生 View。使用 Compose 或 SwiftUI 的 宿主再按需包一层即可,不使用它们的项目也不用增加额外依赖。
另外,各客户端平台基本都有自己的动态照片播放能力,播放器 UI 本身的工作量也不大。直接封装平台原生 能力更简单,也更容易保持系统一致的播放效果和交互。
- Portable Motion Photo v1 核心模型与校验
- 可插拔系统来源 Exporter 注册契约
- 宿主
ResourceLoader与成对资源本地化契约 - Android 共享
AndroidMotionPhotoKit+ 轻量原生 View + 网络 Portable 播放 - iOS 共享
AppleMotionPhotoKit+ 轻量AppleMotionPhotoView - Apple 系统相册或 Apple 原始资源对到 Portable v1 的 Exporter
- Portable v1 到 Apple Live Photo 配对资源的 Preparer
- Manifest 标准 JSON Codec、JSON Schema 与基础兼容性测试
- iOS 可取消 Loader、共享请求调度、预取和 Apple 配对资源缓存
- iOS XCFramework 与本地 Swift Package 制品脚本
- Portable Motion Photo 保存为系统相册 Live Photo
- Android 系统 Motion Photo / 原始资源对到 Portable v1 的 Exporter
- Portable v1 保存为 Android Motion Photo 1.0 系统相册资源
- Android Maven/AAR 制品脚本
- HarmonyOS 系统相册到 Portable v1 的 Importer
- HarmonyOS 原生
PortableMotionPhotoView与 ByteKMP HAR 产物 - 跨平台正式兼容素材与 Manifest Fixture 测试集
motionphoto-core/ 来源、Portable v1、资源加载和公共播放契约
motionphoto-android/ Android Exporter、共享 Kit、原生 View 与系统相册保存
motionphoto-apple/ Apple Live Photo Exporter、配对资源生成与 PHLivePhotoView 播放器
motionphoto-harmony/ ByteKMP 共享接口、ArkUI 原生能力与 HAR 构建
harmonyApp/ 独立鸿蒙验证 App
docs/ 架构、API 契约与 Portable 格式
iOS 技术验证 App 位于 iosApp/,可以从系统相册选择 Live Photo,也可以加载自行配置的网络资源对。
Demo 只构造输入并实现 MotionPhotoResourceLoader;共享 AppleMotionPhotoKit 在库内完成“下载调度 →
平台 Exporter → Portable v1 → Apple 配对资源 → PHLivePhotoView”的完整链路。它通过 Xcode 构建,
不进入库模块的 Gradle 公共 API 兼容范围。androidApp/ 同样只实现宿主 Loader,可验证原始资源对、
网络原始资源对、Android Exporter 和库级原生 View。
三个平台 Demo 中的网络地址使用 example.com 占位。运行网络示例前,请替换为你有权公开使用的 JPEG、
MP4 地址及对应 Manifest;本地测试素材目录默认保持为空。
harmonyApp/ 是独立鸿蒙 Demo,不依赖外部宿主工程。它只实现单资源 Loader,HAR 内共享
HarmonyMotionPhotoKit 负责网络调度;也能从系统相册选择动态照片并转换为 Portable v1,最终交给
PortableMotionPhotoView 展示。
./gradlew :motionphoto-core:allTests \
:motionphoto-android:testAndroidHostTest \
:androidApp:assembleDebug \
:motionphoto-apple:compileKotlinIosSimulatorArm64构建可交付的 iOS XCFramework 和本地 Swift Package:
./scripts/build-ios-distribution.sh产物输出到 dist/ios/。正式接入方式与生命周期约定见
iOS 接入文档。
构建 Android 本地 Maven 仓库(默认版本 0.1.0-alpha.4):
./scripts/build-android-distribution.sh产物输出到 dist/android/repository/。正式接入方式见
Android 接入文档。
构建鸿蒙 HAR 和独立 Demo HAP:
./harmonyApp/build-demo.shHAR 输出到 motionphoto-harmony/output/,未签名 Demo HAP 输出到 harmonyApp/main/build/。真机运行时可用
DevEco Studio 打开 harmonyApp/ 并配置本机调试签名。
正式接入方式、权限配置和资源生命周期约定见 HarmonyOS 接入文档。
详细说明见架构设计、公共 API 契约和 Portable Motion Photo v1。
MotionPhotoKit is licensed under the Apache License 2.0.