摘要生成中...
AI 摘要
Hunyuan-lite

动态图片不是最近才有的东西,但我也是这两年才慢慢接受并使用它。具体接受它原因有很多,比如各大 APP 开始支持动态图片展示、去年换了一个拍照好看的手机、近期各种游游游拍拍拍……为了使我的博客以更丰富的形式展现生活状态,我开始有了让我的博客支持动态图片的方法。

Readme Card

本文介绍了我新开发的一款支持在 Hexo 博客中插入实况照片(动态图片,Live Photo)的插件 @uuanqin/hexo-filter-live-photo。该插件最简单的使用方式为:只要提供一个常规的视频链接,就能在博客中插入一张动图!

Live Photo
Live
鹅岭公园的小猫 Kitten in Eling Park

更多展示示例详见:站内文章动态图片测试

期待你的 Star⭐~ 欢迎 Issues & PRs!诚挚邀请大家体验插件🎉~

会动的照片诞生在 10 年前

实况照片通过记录快门前后一两秒的动态影像和声音,将静态摄影的瞬间捕捉能力扩展到了动态的时间维度。

现在的比较完善的「会动的照片」功能最早出现在 2015 年苹果手机上,叫 Live Photos;安卓这边与之对标的相似功能叫「实况照片」或「Motion Photos」。如果还要往前追溯,那就是 2012 年诺基亚推出的「动态图片」功能,或者是各类厂商的「连拍」「照片集」功能,但是那时候的动态照片还没有声音,整体功能还处于探索阶段。

实况照片对于年轻人来说是一种很有新鲜感和展现生命力的照片形式。——OPPO

近两年来,随着微信、小红书和抖音等社交平台对实况照片的陆续支持,加上手机厂商对实况照片技术的不断跟进,这项 10 年前的技术又在最近小火了一把。

为了便于后文叙述,我把 实况照片、动态照片/图片、Live Photos 就混在一起讲咯,希望大家不要介意…

实况照片的本质就是静态图像加上动态的影像的融合记录。不同厂商对于实况照片都有各自的实现:

厂商 实况照片的实现方式
苹果 早期版本中,一张实况照片由两个独立的文件组成,通过相同的文件名(不含扩展名)和特定的元数据 ID 进行关联。现代模式中,苹果通过一个 HEIC 容器封装实况照片。
谷歌 将视频嵌入在 JPEG 文件的 MP4 容器尾部(Micro Video)。现在新机型已经使用新标准(Motion Photo)。
三星 类似于早期 iOS,使用独立的视频文件或嵌入式结构。现在新机型已经使用新标准(Motion Photo)。
小米 现在 HyperOS 3 支持安卓动态照片新标准(Motion Photo)。

实况照片的实现技术划分为苹果和安卓两大阵营,下面的表格对比了两大手机阵营动图实现方式的差别:

阵营 苹果 安卓
封装格式 HEIF JPG
内容形式 照片 + 视频 照片编码数据 + 视频编码数据
编辑支持 编辑过后不会丢失数据 一经编辑就会丢失视频数据
还原支持 编辑过后仍可还原 一经编辑不可还原
压缩效率 压缩效率高,文件较小 压缩效率低,文件较大
导出支持 不支持 .heif 导出 支持 .jpg 直接导出
兼容性 编解码特殊,兼容性差 容易被魔成独占格式
内容提取 原文件导出即可分离出图片和视频 提取封面图片或视频困难

安卓阵营中各厂商实现方式也有所不同,且互相不一定兼容。不同厂商还可以需要动态照片中有自己的「私货」元数据才能识别动态照片。

博主使用的是小米手机。对于小米手机的动态图片的实现,我也有简单的探索。通过实操可以得到的现象和确定的事实如下:

  • 小米手机拍摄出来的动态图片命名格式为 MVIMG_YYYYMMDD_XXXXXX.jpg。这个前缀通常是用来标识 MVIMG 的。
  • 小米手机拍摄出来的的动态图片以 JPG 结尾,视频数据存在于图片中,没有链接到其他文件夹中隐藏的视频。
  • Windows 自带的「照片」软件可以自动识别为动态图片。
  • 这个网站 可以直接解析小米的动态图片。
  • 小米相册中的部分编辑操作会导致「超动态预览关闭」,将动态图像变为普通静图,但即使未进行任何编辑,生成的静态图片体积反而会增加。

在一瞬间,我闪过了想用 站内文章010 编辑器 一探究竟的可怕想法,但被「上过 🐢⭐课」的回忆给打消了。

小米动图封装思路其实是把视频封装进了 JPG 图像的结尾,并通过 XMP 元数据标记出视频的位置,没有夹带「私货」元数据。在受支持的图片查看器中会显示为「动态照片」,在不受支持的查看其中仅显示封面图片。

image.png

现有技术调研

对于苹果的 Live Photos,官方提供了解析库 LivePhotosKit JS 以方便网页解析动图。基于这个解析库之上,不少开发者已经为其开发了在 Hexo 解析动图的插件。比如:Hexo 中实现 Live Photos 支持。小米并没有提供相关的官方 JavaScript 库,只提供有 APP 应用集成 SDK——但这并不是我想要的。

Hexo 已经有开发者开发过类似的插件,或者有过类似的实现。@程小客 开发了一款 JavaScript 库 Livephoto.js 以及对应的样式,使我们可以轻松的在网页上复现手机拍摄的动态图片的效果。它的基本使用方法为,在网页中引入写好的 JavaScript 和 CSS 文件,然后通过在插入 HTML 代码块进行实现:

1
2
3
4
5
6
<div class="live-photo-container">
<img src="path/IMG.jpg" alt="描述文字" class="live-photo-static" />
<video muted playsinline class="live-photo-video">
<source src="path/VID.mp4" type="video/mp4" />
</video>
</div>

HTML 代码块灵活性非常高,我们只需要填充好视频链接和封面链接就可以完成一张动图的插入了。对于 Hexo 用户,@程小客 基于这个库还特别开发了 Hexo 插件 @cykzht/hexo-live-photo,支持通过 Hexo 独有的「标签插件」语法完成对动图的插入:

1
{% livephoto image_path video_path %}

了解我的朋友都知道 [1],虽然我用 Hexo 博客,但我宁愿自己开发工具,也不会用 Hexo 的标签语法的,因为它的语法侵入性太大了。但是对于「动态图片」这个新事物,现有的 Markdown 语言根本不支持,所以我还需要思考应当以何种形式,在最大限度保证语法纯洁和各平台兼容性上实现动图的插入。

自己开发插件前的构思

我首先想到的的是直接以 Markdown 图片格式完成动图插入,这可以说是很贴合 Markdown 原生的方式了,毕竟都是「图」嘛。我们只需要在 Obsidian、Hexo 平台开发相应的插件,自主解析图片是否为动图,然后按需渲染。这种方式的好处为:

  • 方便文档编写者插入图片;
  • 方便对图片的管理;
  • 不会破坏现有的所有文档工作流。

这种方式对文档编写者来说是最不打扰的方式,但是弊端也明显。还需要考虑:

  • 如果解析动图的职责将交给客户端,那么会有网页性能挑战。没有像苹果那样的现成库,开发成本较高(懒);
  • 解析动图的职责可以交给博客框架,在编译期间解析。但是这同样也有开发成本(懒),而且如果后续开发 Obsidian 插件,思路可能又不太一样。

基于上述考虑,我也放弃了直接以 Markdown 图片格式完成动图插入的想法,心中也放开一定的界限,允许有限语法侵入。这样一来,其实上面调研过的 Livephoto.js 直接嵌入 HTML 代码块是可以接受的了:

  • Hexo 允许第三方 JavaScript 和 CSS 接入;
  • Obsidian 可以引入 CSS,对动图有一个「降级」的封面展示;
  • 通过 Obsidian 的 scaffolds 设定模板,插入 HTML 模板也不麻烦。

但其实我还是想花费同样的语法侵入成本,保持一定灵活性的同时,用更优雅的方法实现动图的插入。毕竟在 Markdown 块插入一堆 HTML 显得过于「雷霆原生」了💩。于是,我的目光看向了「自定义代码块」。

自定义代码块的实现形式是在原先 Markdown 代码块语法中,将标记渲染语言位置中的词换成自己自定义的词,通过识别块进行自主的参数解析(通常是 YAML 的形式)和渲染。在 Obsidian 第三方插件中,很喜欢用这种形式实现各种各样的拓展功能。

1
2
3
4
```custom_name
key1: arg1
key2: arg2
```

大致思路为,通过正则识别块,用 YAML 库解析参数,在 Markdown 渲染前完成自定义渲染内容的插入。代码块怎么看都比 HTML 顺眼,遇到降级显示时也不复杂,一眼看出是啥。

因为我之前有过开发 Hexo Filter 插件的经验 [2],加上现在也有前人现成的 Livephoto.js 库,和 AI 充分讨论后判定为「可行」,于是新插件就着手开发了~

插件介绍

前面铺垫了这么多,终于进入正题了!

@uuanqin/hexo-filter-live-photo 是由我开发的一款方便用户在 Hexo 博客插入实况照片的插件,插件通过识别 livephotolive-photo 自定义代码块中的视频地址及其他参数,完成可定制的动图插入。主要功能有:

  • 基本功能:动图的展示和播放设置、微信环境优化。也就是 Livephoto.js 库中的基本功能,具体而言:
    • 指定封面链接
    • 指定图片 ALT 文本
    • 动图标题
    • 动图宽度和高度的指定
    • 微信浏览器环境识别
  • 视频加载时有加载效果。
  • 支持配置动图图片循环播放。
  • 支持交互式声音按钮,并优化声音播放体验。
  • 其他在视觉、性能以及用户体验方面的优化。
插件不支持 Hexo 的「标签语法」

如有标签语法插入需求,请参考这个项目:cykzht/hexo-live-photo

Live Photo
Live
鹅岭公园的小猫 Kitten in Eling Park

其他各种动图预览详看这篇文章:站内文章动态图片测试。最小可运行的代码块插入示例为:

1
<div class='live-photo-container' style="width:fit-content; max-width:100%;"><img src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' alt='Live Photo' class='live-photo-static' style='display:none; max-width:100%; height:auto; opacity: 1;'><video muted playsinline autoplay  preload='auto' class='live-photo-video' src='/live-photo.mp4' style='max-width:100%; height:auto;'></video><div class='live-sound-toggle' title='Sound Toggle'><svg class='sound-on-icon' viewBox='0 0 1024 1024' width='15' height='15' fill='currentColor'><path d='M949.418667 502.369524v64.024381c-10.800762 120.539429-82.115048 223.646476-183.344762 278.723047L732.330667 780.190476A280.30781 280.30781 0 0 0 877.714286 534.381714a280.380952 280.380952 0 0 0-153.941334-250.319238l33.694477-64.926476c105.764571 53.808762 180.833524 159.305143 191.951238 283.233524z m-145.627429 24.576a218.819048 218.819048 0 0 1-105.618286 187.66019l-33.889523-65.097143a145.700571 145.700571 0 0 0 66.364952-122.563047 145.700571 145.700571 0 0 0-68.583619-124.001524l33.792-65.048381a218.819048 218.819048 0 0 1 107.934476 189.049905zM611.547429 205.04381V829.19619c0 49.859048-61.561905 74.044952-96.060953 37.741715l-160.353524-146.383238H159.305143a73.142857 73.142857 0 0 1-73.142857-73.142857V403.407238a73.142857 73.142857 0 0 1 68.827428-73.020952l4.924953-0.121905 197.680762 3.291429 157.915428-166.229334c34.474667-36.327619 96.060952-12.117333 96.060953 37.741714z'></path></svg><svg class='sound-off-icon' viewBox='0 0 1024 1024' width='15' height='15' fill='currentColor'><path d='M611.547429 470.918095v358.253715c0 49.859048-61.561905 74.044952-96.060953 37.741714l-156.793905-143.164953 252.830477-252.830476z m209.92-209.944381a353.01181 353.01181 0 0 1 127.951238 241.371429v64.048762c-10.800762 120.539429-82.115048 223.646476-183.344762 278.723047l-33.767619-64.902095A280.30781 280.30781 0 0 0 877.714286 534.381714a279.893333 279.893333 0 0 0-108.251429-221.354666l52.028953-52.053334z m-108.495239-63.878095l51.736381 51.712L213.016381 800.475429l-51.712-51.687619L712.97219 197.071238z m11.532191 160.865524a218.819048 218.819048 0 0 1 79.286857 168.96 218.819048 218.819048 0 0 1-105.618286 187.684571l-33.889523-65.097143a145.700571 145.700571 0 0 0 66.364952-122.563047c0-47.957333-22.918095-90.453333-58.221714-116.906667l52.077714-52.077714z m-112.956952-152.868572v24.478477L127.73181 713.386667a73.142857 73.142857 0 0 1-41.569524-65.999238V403.407238a73.142857 73.142857 0 0 1 68.827428-73.020952l4.924953-0.121905 197.680762 3.291429 157.915428-166.229334c34.474667-36.327619 96.060952-12.117333 96.060953 37.741714z'></path></svg></div><div class='live-badge top-left'>Live</div><div class='live-loading'></div><div class='live-error-placeholder'><svg t='1776112172334' class='icon' viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' p-id='8506' width='200' height='200'><path d='M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m-191.488 486.521904c134.436571-93.037714 248.539429-93.037714 382.976 0l-41.642667 60.14781c-109.372952-75.727238-190.317714-75.727238-299.690666 0zM414.47619 341.333333a48.761905 48.761905 0 0 1 48.761905 48.761905v73.142857a48.761905 48.761905 0 1 1-97.523809 0v-73.142857a48.761905 48.761905 0 0 1 48.761904-48.761905z m195.04762 0a48.761905 48.761905 0 0 1 48.761904 48.761905v73.142857a48.761905 48.761905 0 1 1-97.523809 0v-73.142857a48.761905 48.761905 0 0 1 48.761905-48.761905z' p-id='8507'></path></svg></div></div>

更多参数说明详看 项目文档。欢迎大家试用并反馈意见~

后记

插件开发周期大概为 5 天(全天),1 天调研、3 天开发、1 天审查和各种收尾。这次的项目实践也是有一次的 AI Coding 实践,虽然是基于前人开发的 JavaScript 库上做的,但是在各种功能改进的过程中,自己也慢慢把源代码给消化了,学习到更多样的 Hexo 插件开发方式。

通过近期几项 AI Coding 实践,我总结了一些经验要点:

  • 开发功能前,需要对需求进行讨论和打磨。确定功能实现内容和实现的方式;
  • 做好版本管理,必要时重做回退;
  • 必须人工审查 AI 给的代码,核对仔细修改位置和修改细节,理解代码含义;
  • 小功能,小修复逐步迭代;
  • 最后一步,检查代码性能,检查代码的可维护性。

这个插件的开发解决了动图在 Hexo 中的展示,但在实现 Hexo-Markdown-Obsidian-COS「打通」前还有一定的路要走。不过现在我已经看见了前人铺好的台阶🌟,敬请期待我的下一个项目~

本文参考

工具:regex101: build, test, and debug regex


  1. 不了解的朋友也可以从这两篇文章中窥之一二:站内文章【开发杂记】第一次开发 npm 插件站内文章【开发杂记】第一次写 Hexo 插件↩︎

  2. 可以看看我写的 Hexo 插件:站内文章Hexo 博客适配 Obsidian 新语法站内文章自定义 Hexo 中的超链接样式↩︎