React 新玩具,为 web 而生的实况照片处理应用

:webui header

新玩具 motion live photo webui,或称 MLPW,名字来自实况照片(motion photo)和动态照片(live photo)各取首单词,也有『律动生活』的双关寓意。

继博客的实况组件完工后,我就开始琢磨类似R2 uploader的实况照片 WEBUI 上传工具。最开始的想法很简单:利用 ffmpeg wasm 压缩图片和视频(如果是谷歌系的单 jpg 文件格式就先拆分),然后通过浏览器 XHR 上传到云端。

既然要做单页应用 (SPA),那必须得是 React 啊!正好拿这个项目练练手,顺便入了 React 的门。所以当初目的有多纯粹,完成体的功能就有多大杂烩🥘,📌比如合成谷歌版的单文件动态照片,视频转实况(截图抽帧作为封面),Web Hook UI 等等・・・完整特性可以去看看项目的 README。

作为 react 菜🐣选手,开发过程免不了磕磕绊绊,有瑕疵但也做了各种补救。📌比如 ffmpeg 转码会概率性失败,不是报内存不足,就是莫名其妙卡住。所以在转换过程中增加了中断按钮,以及允许手动调整命令行参数的选项。浏览器不是万能的,有些硬核转码任务,📌比如把图片转成 AVIF 或 HEIF,或者生成 AV1 编码的视频,它确实力不从心。💡因此我又想了一个招:在上传模块继成了☁️云转码功能,只需配置好自定义 API 即可。至于上传操作的同源策略限制,可以通过自建 CORS 代理,或者更直接的:先下载分离好的图片视频,手动转码后再用其他工具上传。

关于实况照片和动态照片各自的原理和区别,这里就不展开了。接下来,让我们深入 MLPW 的各个功能模块,聊聊那些小点子、小细节。

🗝️文件载入:一切的开始

一切操作都始于文件导入。理论上,常见的图片视频格式都被支持。原生 heic 无法在浏览器显示,MLPW 会自动调用 heic-to转换成常规 jpg,其他格式图片会先尝试作为实况照片来提取🖼️和🎥,失败后将被视为静态图片导入。所有导入的文件(无论是否经过提取转换)统一都被视为 **{RAW}**类,包括通过文件转换模块提取的🖼️。新导入的 RAW 默认覆盖旧的,并且对应的 {CONVERTED} 类文件也会销毁。有一个特例,就是实况照片中提取的视频在覆盖原有 RAW 视频之前会弹窗确认,毕竟导入图片却覆盖视频有点反直觉🙅。

📺多媒体显示:所见即所得

面板展示
LIVE

文件导入成功后,你会看到三个标签页的状态发生了变化。如果遇到 H265 这类比较新的视频编码格式播放失败,也别慌,这并不会影响后续的转码操作,后续转码成功的 CONVERTED 视频会自动替换掉无法播放的 RAW 视频,其他情况下🎥标签仅显示 RAW 视频。🖼️标签在转码后会显示 compare slider,类似 Squoosh 的滑动控件,方便比对压缩前后的画质差异。:livephoto:实况控件每次切换都会刷新,且优先加载 CONVERTED 文件。

为了带来最原汁原味的体验,实况照片的显示我直接用了苹果官方的 LivePhotosKit JS。,用 CSS 也能实现类似的效果,而且优点多多,原理也不复杂,核心就是让原图淡出的同时,放大并淡入视频。

⚙️文件转换:核心功能

功能示意图
这里的大部分操作都需要❤️‍🔥ffmpeg wasm❤️‍🔥驱动,为了方便,我直接用了 unpkg 的源,第一次加载成功后,后续都会从浏览器缓存中读取,无需重复下载。切换单/多线程模式后需要重新加载一次库文件,所以如果遇到奇奇怪怪的转码问题,不妨试试切换线程或者重载一下,通常比刷新整个页面更有效。转码前,记得检查下拉菜单的选项,默认是同时处理🖼️和🎥的。如果你想在执行前微调 ffmpeg 参数,比如用 -qscale:v 来控制图片压缩质量,只需勾选 ✔️手动调参 即可。⚠️友情提示:不要修改参数中的输入输出文件名。点击下载按钮会展开一个下拉菜单,再次点击标题即可开始下载。旁边的 按钮可以让你修改文件名(不含扩展名),这个名字会同时用于下载和上传

按钮上方的 2x2 标签用于展示🖼️和🎥的文件大小和(预期)分辨率,箭头左边是 RAW,右边是 CONVERTED。转换完成后,右侧的→会变成醒目的绿色。

多线程开关右侧的三个按钮,分别对应于调整尺寸、🎬视频转码设置和🖼️图像转码设置

  • 曾经被这篇文章误导,以为 LivePhotosKit 对分辨率和格式有严格要求。实际上,它并不限制分辨率,也不强制要求 JPG 格式,但 H265 编码的视频确实播不了,哪怕浏览器本身支持。尽管如此,我还是为网页显示设置了友好的默认最大尺寸:🖼️1008 x 1344 和🎥720 x 960(保持宽高比)。你随时可以通过浮窗右上角的⟳按钮重置,或者点击⛶保持原始尺寸。

  • 🎬视频设置项包括是否保留音频,设置转码格式(mp4, mov 和 webm)以及精确剪辑。把🎥进度条拖到你想要的位置,点击📸就能录入开始和结束时间戳。如果 RAW 视频无法播放,你可以手动输入时间,或者先转码再剪辑。此外,这里选择的转码格式也会影响云端转码后返回文件的扩展名(除非你在 API URL 里写死了)。

  • 🖼️设置项包括转码格式(jpg,png 和 webp),目前没有提供输出质量之类的参数,实在有需求可以通过手动调参曲线救国。HEIC 质量系数只在导入 heic/heif 图片时起作用。截图功能可以让你从视频中轻松提取封面,默认会抓取你打开浮窗时🎥的播放进度。你可以选择从原始视频或转码后的视频中截取,或者只是修改时间戳数值,这个值会用于实况组件的 data-photo-time 属性,以及合成实况照片时 XMP 标签里的相关字段。

🚩接下来的模块都相对独立,点击模块标题可折叠/展开。

✨合成实况:优先适配 OPPO

合成模块
这个功能是最后加上的,目标很明确:用任意图片🖼️和视频🎥,合成本地相册能识别的实况照片橘子Jun实现了一个简易合成函数ƒ,能够被:google-photo:谷歌相册、windows 相册识别,关键点在于写入正确的 XMP 标签。测试发现,原版ƒ合成的照片能够被最新版抖音和:rednote:小红书识别,但 ColorOS 系统自带的相册不行。一次偶然的机会,我发现小红书保存的单文件实况照片居然能被我的手机相册完美识别!用 WinHex 一番研究后,我找到了秘诀:只要把小红书保存图片的 EXIF 部分,替换到 MLPW 合成的文件里,就能实现完美兼容🎉!

所以,如果你用的是 oppo 系手机,只需要切换到对应的 XMP meta 预设,就能导出正确的实况照片。 由于手头没有其他牌子手机,适配计划只能暂时搁置了。不过去线下体验店试了下,小米、荣耀在小红书上导出实况都只是分开的两个文件,那我估计就更难搞了🤔。最近 ColorOS 更新了一个视频🎥:livephoto:的功能,但最多只允许 3 秒,而 MLPW 导出的实况没有时长限制😎。

如果导入的是一张实况照片,MLPW 默认提取内嵌的 XMP 字段到输入框,如果提取失败,则会显示当前选中的预设值。点击右上角的可以清空输入框,点击⟳则能一键恢复到图片加载时的初始 XMP 数据。

☁️上传与云转换:牵手云端

上传和云转换
Web Hook 功能通过 XHR 实现,支持 POST, PUT, DELETE, GET 四种 HTTP 请求。你可以自定义请求头,POST 方法还支持设置请求体。比如,使用 WebDAV 协议时,你需要用 PUT 方法,并加上两个关键的请求头:Authorization: Basic btoa(USERNAME:PASSWORD)Content-Type: application/xml。点击右上角的💾可以保存当前的服务器配置(JSON 格式),保存后会出现在 API URL 的下拉菜单中。旁边的⿻按钮则用于配置的导入和导出。如果你剪贴板里有有效的配置,它会直接帮你写入 local storage 或询问是否覆盖;如果没有,它会主动把当前配置复制到剪贴板,方便你在不同设备间迁移配置。

目前,我已经成功测试了📌Junze 大佬的 Dooong 公益图床、📌R2 Uploader 以及📌一个PHP 脚本:linux-do:。理论上,只要是常规的 RESTFul API 都能兼容。在 API URL 中,你可以用{filename}来代表文件名,在请求体中用{File}来代表上传的文件本身。最大的挑战还是跨域问题,一个可行的方案是将其编译成本地 App,或者借助 CORS 转发服务器🖧来解决。

为了方便使用上传好的文件链接,下拉菜单里也安排了一键复制文件名的按钮✌。

云转换️️☁️的本质是利用 GET 请求来访问那些基于 URL 的转码服务。📌知名的公益服务WebP Cloud ServicesCloudflare Image都提供了一定的免费额度。每个免费账户每月能进行 5000 次免费转换,使用起来非常简单:

  1. 首先,进入 Cloudflare 后台,在 Images👉Transformations 页面为你的域名启用转换服务。
  2. 上传图片到 R2 云存储,假设你的图片直链是🔗https://image.yourdomain.com/raw.jpg
  3. 在链接中加入转换参数,📌比如,要转换为 webp 格式并设定压缩质量为 75,链接就变成了: https://image.yourdomain.com/cdn-cgi/image/format=webp,quality=75,fit=scale-down/raw.jpg

为了能明确指定转换后的格式,最终填入 API URL 的格式应该是这样的: https://image.yourdomain.com/cdn-cgi/image/format={webp},quality=75,fit=scale-down/{filename}。其中{filename}会被替换为 RAW 文件名,而{webp}则会作为云转换后文件的后缀。

📖 日志:实时记录

日志模块
所有操作记录都会在这里显示,滚动到最底部还会触发自动滚屏,方便实时追踪。

经过一个多月的学习和打磨,MLPW 在我看来已经达到可用的程度,所以写了这篇文章,分享并记录。当然,它还缺少 OPPO 以外机型的兼容性测试📱,也可能存在一些隐藏在特殊场景下的 bug🐞。目前,我用得最多的功能就是分离并压缩手机拍的实况照片,然后上传到博客当图床。未来,我最想加入的功能就是裁切,实现类似网页版 𝕏 那种酷炫的媒体编辑效果。

 crop media