我开发了一款动态照片查看、转换与分享 APP
问题背景:
不同品牌的实况照片格式不同,在不支持动态照片的 APP 中(比如 QQ)无法正常分享。即使发送原图,保存到本地后也只能看静态图片。
软件功能:
- 自动扫描出手机里所有品牌的动态照片,通过 QQ 等不支持动态照片的APP保存对方原图后,可以直接播放
- 一键转换成自己手机支持的格式,使得在相册中可以直接播放
- 一键提取视频,把内嵌视频当成普通视频用
- 生成动态照片分享链接,让对方直接在网页中播放和下载
目前功能 2 仍在开发,已完成对 OPPO、vivo(新格式)、小米、华为(非纯血鸿蒙)/荣耀的适配,功能 1 3 4 所有品牌可正常使用。现邀请感兴趣的同学进行内测,想体验的可以联系我,要求手机发布时间不大于两年
(目前 APP 不支持纯血鸿蒙和苹果,后续是否适配看心情)
网页分享链接示例:http://animphoto.ylin314.cn/s/06d977f09b56
下面是技术讲解
动态照片到底是什么
手机里的动态照片(也叫 Live Photo)并不是一种统一格式
小米、OPPO、vivo(新格式)、华为(非纯血鸿蒙)和荣耀都采用了“静态照片 + 视频”的组合思路,也就是在照片后面直接嵌入视频,但它们在元数据、视频定位方式和厂商私有扩展上各不相同
vivo(旧格式)、华为(纯血鸿蒙)和苹果则采用了照片和视频分开存储的策略
两种技术最大的区别就是,在 QQ 等没有适配动态照片的软件中,如果勾选“发送原图”,前者会连带视频一起发送,而后者只会发送静态照片,接受者想尽一切手段都无法获取视频,简直是活全家了
本文基于四家相册 APK 的逆向结果、四家 Live Photo 原文件解析以及瞬影 APP 的开发过程,总结这前者四种格式的文件布局与读取方法
嵌入式动态照片结构
从文件结构看,这四家安卓动态照片都可以抽象成:
1 | JPEG 主图 |
判断一张 JPG 是否为动态照片的关键是XMP 元数据,它告诉软件照片是否有视频,视频在什么位置,而视频本身则是标准 MP4。
小米
动态照片的 XMP 元数据(动态照片相关字段)如下:
1 | <rdf:Description rdf:about="" |
命名空间
小米的命名空间没有自己的私有字段,全部是 Google 官方字段
身份标记
GCamera:MotionPhoto="1":Google 规范下的动态照片开关,1 表示是动态照片
GCamera:MotionPhotoPresentationTimestampUs:封面帧的时间戳(微秒),用来标记哪一帧是封面
小米有两种版本动态照片,新机型走 MotionPhoto 容器,老机型走 MicroVideo
资源清单
资源清单是最核心的部分,标记了主图、HDR 增益图、视频
Item:Length 表示从视频起点到文件末尾的反向长度
小米的动态照片格式是最接近 Google 官方格式的,其读取和转换都是最便捷的
读取与解析
直接看 GCamera:MotionPhoto 或 GCamera:MicroVideo 是否为 1 即可,非常简单
定位视频
- 新版:Container 里按 Semantic 找视频项,videoOffset = 文件大小 − Item:Length,之后校验 videoOffset + 4 处是否为
ftyp - 旧版:videoOffset = 文件大小 − MicroVideoOffset,同样校验
ftyp - 兜底策略:JPEG EOI(FF D9) 之后扫描
ftyp,videoOffset =ftyp位置 − 4
转换
转换也是十分的便捷,直接保留图片和视频原字节,然后按照上述格式重写 XMP 即可
OPPO
vivo 和 OPPO 这两家步步高厂商格式就比较乱套了
OPPO 将动态照片称为 OLive Photo。它的基础结构仍然是 JPEG 加 MP4,但比纯 Google Motion Photo 多了一组 OPPO 私有元数据
1 | <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core Test.SNAPSHOT"> |
OPPO 的 XMP 数据在 Google 官方的基础上添加了 xmlns:OpCamera 以及大量 OpCamera 相关字段,比较重要的如下:
OpCamera:VideoLength:MP4 的长度Item:Length:MP4 + OLive 私有尾数据的总长度
读取与解析
XMP 里 GCamera:MotionPhoto=="1" 且出现任意 OpCamera: 标签
定位视频
- Container 里按 Semantic 找视频项,videoOffset = 文件大小 − Item:Length,之后校验 videoOffset + 4 处是否为
ftyp - 兜底策略:JPEG EOI(FF D9) 之后扫描
ftyp,videoOffset =ftyp位置 − 4
OPPO 没有旧版字段,只有新版
转换
保留主图 + GainMap + MP4,写 OPPO 的 XMP 和 EXIF,OLive 私有尾数据可不写(OPPO 图库不要求,实测可播)且 Primary 项要带 Length="0" Padding="0",要求非常的多
对的,ColorOS 新图库会检测 EXIF 中的 UserComment 项,必须是 oplus_* 才给识别动态照片,神了
vivo(新格式)
vivo 与 OPPO 相同,也添加了自己的私有元数据
1 | <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.0-jc003"> |
vivo 最大的特点有两个:
尾部有两个 UUID 盒
视频起点带有
cameralbum!标记,且Item:Length覆盖的是 纯 MP4 + 两个 UUID 尾盒的长度,不含前面 10 字节的cameralbum!
也就是 videoOffset = 文件大小 − Item:Length 公式仍成立
读取与解析
XMP 里 GCamera:MotionPhoto=="1" 且出现任意 VCamera: 标签
定位视频
- Container 里按 Semantic 找视频项,videoOffset = 文件大小 − Item:Length,之后校验 videoOffset + 4 处是否为
ftyp - 兜底策略:JPEG EOI(FF D9) 之后扫描
ftyp,videoOffset =ftyp位置 − 4
vivo 也没有旧版字段,只有新版
转换
不仅要保留原有图片 + GainMap + 视频,还要添加两个 UUID 盒,还有中间的 cameralbum! 标记,且每次都要生产新的 id
华为(非纯血鸿蒙)/荣耀
华为(非纯血鸿蒙)和荣耀均属于 OpenHarmony 旧式单文件动态照片格式,通常不依赖 MotionPhoto XMP
照片的尾部有一段数据:
1 | v2_f40 |
每个字段都使用空格补齐到 20 字节
定位视频的方法如下:
1 | liveSize = parseInt("LIVE_" 后面的数字) |
四家格式总览
| 品牌 | 主要存储形态 | 动态标记 | 视频定位方式 | 主要私有数据 |
|---|---|---|---|---|
| 小米 | 单文件:JPEG + MP4 | GCamera:MotionPhoto 或旧版 MicroVideo |
XMP 中的 Item:Length 或 MicroVideoOffset 反向计算 |
无 |
| OPPO | 单文件:JPEG + MP4 | GCamera:MotionPhoto + OpCamera:* |
XMP 中的 Item:Length |
oplus_* EXIF、OLive 私有尾数据 |
| vivo | 单文件:JPEG + MP4 | GCamera:MotionPhoto + VCamera:* |
XMP 中的 Item:Length 或 MicroVideoOffset |
cameralbum!、两个 vivo UUID 扩展盒 |
| 荣耀 | 单文件:JPEG + MP4 + 尾标 | 通常没有动态 XMP | 文件尾 LIVE_ 尾标反推 |
OpenHarmony 旧格式的 20 字节固定尾标 |
我说真得买小米,最听 Google 话的厂商
非嵌入式动态照片结构
没适配呢,你先别急