惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
H
Help Net Security
GbyAI
GbyAI
博客园_首页
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
月光博客
月光博客
美团技术团队
B
Blog RSS Feed
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
博客园 - 叶小钗
有赞技术团队
有赞技术团队
T
The Blog of Author Tim Ferriss
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed

Dejavu's Blog

甲骨文 ARM 实例部署 Gemma 4 模型 Headscale + Tailscale 组建虚拟专用网 在 Linux 上使用 Yubikey OpenPGP 应用 BuyVM VPS 块存储挂载教程 Alpine Linux 服务器配置指南 Alpine Linux 安装 Cloudflared Docker 多容器共享中心数据库 安装 Komari 服务器监控工具 Scaleway VPS 安装 Debian Linux Debian 13 下部署 AsmBB 论坛 使用 Kopia 自动化备份服务器数据 给 Docker 启用 IPv6 支持 Netcup 服务器安装自定义 ISO 镜像 烽火 HG5582A 光猫开启桥接模式 Docker 自托管 Shlink 短链服务 部署 Obsidian LiveSync 实时同步服务指南 我的 2025 年不完全回顾 Linux 下 Intel 核显驱动配置与硬件加速 Fedora Linux 安装配置记录 2025 年优雅地自托管 RSS 服务 Woodpecker CI 和 Gitea 实现 Hugo 自动部署 Gitea/Forgejo 集成 Woodpecker CI/CD 在 Blinko 中使用 Ollama 作为 AI 供应商 Docker 部署 Gitea/Forgejo Plausible CE 启用城市级地理位置识别 Blinko 开源 AI 知识库 Docker 部署指南 Netcup 免税账号注册及购买服务器全记录 Docker 自托管 Cloudreve Pro 私有网盘服务 GiffGaff SIM 卡使用体验和注意事项 简体中文互联网在变得糟糕吗?
在 Hugo 站点中优雅地展示实况照片
Dejavu Moe · 2025-10-13 · via Dejavu's Blog

前言

Apple 推出 实况照片 (Live Photos) 很多年了,它会在手机拍摄一张照片的同时,记录下数秒时间的有声影像,使得定格的瞬间可以有更多的「现场感」和「故事感」。后来 Google 推出了自己的 动态照片 (Motion Photos),尽管他们在实现原理上还存在着一些不同,但基本上都可以将它们理解为一张静态图片 + 一段有声的瞬间 + 一些其他信息。

今年,随着各个社交平台对实况照片的支持,我们可能越来越喜欢这种更生动有趣的记录方式。本文主要介绍在 Hugo 静态博客网站上优雅地展示 Live Photos 或 Motion Photos 的方式。

值得注意的是,目前在 Web 网站上,.JPG.MP4 仍然是兼容性最好的格式。iOS 设备上的 Live Photos 会在备份导出到非 Apple 设备时,自动拆分为 .HEIC 静态图片和一段 .MOV 视频,因此,如果想在网页上展示 Live Photos,可能还需要进行格式转换,可以使用的工具太多了,本文对此不作展开说明。

目前我已经知道的 Samsung、小米/红米、OPPO/OnePlus 等主流 Android 手机基本都遵循了 Google 的 Motion Photos 规范,细节上的差异不在本文讨论范围内。

至此,我们仍需要将 Motion Photos 手动「拆」成静态图片 + 视频,这里推荐使用 GoMoPho (Google motion photos video extractor) 工具进行拆包。

效果

下面演示的是一张 Motion Photos 在本文最终实现的效果,你可以尝试:

  • 点击 LIVE|声音按钮
  • 在触屏设备上长按图片
  • 鼠标指针悬停在图片上
  • 移开悬停在图片上的鼠标指针

实现过程

我们首先要对 Motion Photos 中的静态图片和视频资源进行提取,下面介绍两款工具,一款直接在浏览器中使用,另一款需下载后在命令行中使用。

MotionFlow

MotionFlow 是我用 AI 辅助构建的一款纯静态的在线 Web 应用,支持批量处理 Motion Photos 并打包下载。你可以 在线体验 ,它是开箱即用的,所有处理操作只在浏览器中进行以保证隐私和安全。

motionflow.webp

GoMoPho

GoMoPho 是一款跨平台的 Google Motion Photos 提取工具, 下载 后解压直接在终端运行(需指定参数)

GoMoPhoConsole.exe d <输入路径> s "<输出路径>" p "*.<文件后缀名>"

举个例子

gomopho.webp

解压完成

gomopho-out.webp

一张 Motion Photo 解包后会得到一张照片和一个视频,它们体积较大,建议上传到图床或对象存储上,并确保其可以通过 直链访问

以 Hugo PaperMod 主题为例,新建一个短代码 motionPhotos.html

{{- $videoSrc := .Get "video" -}}
{{- $imageSrc := .Get "image" -}}
{{- $altText := .Get "alt" | default "Motion Photo" -}}
{{- $uniqueId := now.UnixNano | string | md5 -}}
{{- $imageResource := "" -}}
{{- $localResource := "" -}}

{{- if $imageSrc -}}
    {{- $isRemote := or (hasPrefix $imageSrc "http://") (hasPrefix $imageSrc "https://") -}}

    {{- if not $isRemote -}}
        {{- if hugo.IsProduction -}}
            {{- $localResource = resources.Get $imageSrc -}}
        {{- end -}}
        {{- if $localResource -}}
            {{- $imageResource = $localResource -}}
        {{- else -}}
            {{- $imageResource = resources.GetRemote $imageSrc -}}
        {{- end -}}
    {{- else -}}
        {{- $imageResource = resources.GetRemote $imageSrc -}}
    {{- end -}}
{{- end -}}

<div class="live-photo-container" id="mp-{{ $uniqueId }}"
    {{- with $imageResource -}}
        {{- if and .Width .Height -}}
            style="aspect-ratio: {{ .Width }} / {{ .Height }}; max-width: min(100%, {{ .Width }}px);"
        {{- else -}}
            style="aspect-ratio: 4/3; max-width: min(100%, 700px);"
        {{- end -}}
    {{- else -}}
        style="aspect-ratio: 4/3; max-width: min(100%, 700px);"
    {{- end -}}>
    <video class="live-photo-video" data-src="{{ $videoSrc }}" poster="{{ $imageSrc }}" muted playsinline loop>
        您的浏览器不支持 HTML5 视频。
    </video>
    <div class="live-photo-controls-group">
    <button class="live-photo-control-btn live-photo-toggle-btn" data-state="static" aria-label="Toggle Motion Photo">
        <i class="icon-live"></i><span>LIVE</span>
    </button>
    <button class="live-photo-control-btn live-photo-mute-btn" data-muted="true" aria-label="Toggle Mute">
        <i class="icon-muted"></i>
    </button></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function(){
    var container=document.getElementById('mp-{{ $uniqueId }}');
    if(!container)return;
    var video=container.querySelector('.live-photo-video');
    var toggleBtn=container.querySelector('.live-photo-toggle-btn');
    var muteBtn=container.querySelector('.live-photo-mute-btn');
    var playTimeout;
    const HOVER_DELAY=500;
    var isManuallyControlled=false;
    function updateMuteButtonUI(isMuted){
        muteBtn.setAttribute('data-muted',isMuted.toString());
        var icon=muteBtn.querySelector('i');
        icon.className=isMuted?'icon-muted':'icon-unmuted';
    }
    function startVideo(autoUnmute){
        if(toggleBtn.getAttribute('data-state')==='static'){
            if(!video.src){
                video.src=video.getAttribute('data-src');
                video.load();
            }
            video.style.opacity=1;
            if(autoUnmute){
                video.muted=false;
                updateMuteButtonUI(false);
            }
            video.play().catch(e=>{
                if(e.name!=='AbortError'){console.error("Video play failed:",e);}
            });
            toggleBtn.setAttribute('data-state','playing');
        }
    }
    function stopVideo(){
        if(toggleBtn.getAttribute('data-state')==='playing'){
            video.pause();
            video.currentTime=0;
            toggleBtn.setAttribute('data-state','static');
            if(!video.muted){
                video.muted=true;
                updateMuteButtonUI(true);
            }
        }
    }
    container.addEventListener('mouseenter',function(){
        clearTimeout(playTimeout);
        if(isManuallyControlled)return;
        playTimeout=setTimeout(()=>startVideo(true),HOVER_DELAY);
    });
    container.addEventListener('mouseleave',function(){
        clearTimeout(playTimeout);
        if(!isManuallyControlled)stopVideo();
    });
    toggleBtn.addEventListener('click',function(event){
        event.stopPropagation();
        var state=toggleBtn.getAttribute('data-state');
        if(state==='static'){
            isManuallyControlled=true;
            startVideo(true);
        }else{
            isManuallyControlled=false;
            stopVideo();
        }
    });
    muteBtn.addEventListener('click',function(event){
        event.stopPropagation();
        video.muted=!video.muted;
        updateMuteButtonUI(video.muted);
    });
    video.addEventListener('ended',function(){
        isManuallyControlled=false;
        stopVideo();
    });
});
</script>
<style>
.live-photo-container{position:relative;margin:var(--gap) auto;overflow:hidden;border-radius:var(--radius);cursor:pointer;background-color:var(--code-block-bg);max-height:70vh;}.live-photo-video{position:absolute;top:0;left:0;width:100%;height:100%;display:block;object-fit:cover;}.live-photo-controls-group{position:absolute;bottom:8px;right:8px;display:flex;align-items:center;gap:5px;}.live-photo-control-btn{position:static;background-color:rgba(0,0,0,0.6);border:none;color:white;cursor:pointer;transition:background-color 0.2s,opacity 0.2s;box-shadow:0 1px 3px rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;white-space:nowrap;}.live-photo-control-btn:hover{background-color:rgba(0,0,0,0.8);}.live-photo-toggle-btn{border-radius:20px;padding:5px 9px;font-size:0.85em;}.live-photo-toggle-btn i.icon-live{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAMAAAAOusbgAAAAM1BMVEX///+OjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpOOjpMym3PHAAAAEXRSTlMAMEAQsP/AIKDggHDQkPBQYHOKyH4AAAUWSURBVHgBYhg6gJEJQgNoMcPsWEEYCguQCKiB/a/2lecp1hgm4sx8v3La1huSG0CdB0C3x3b+vm4AiKYGCX4INTKxBl9mqXKxRlgjrNFao+VbghhMk4OzMLTIBPxwAhsA0LxXGoDqj3KNco2oRttPMNMefBCopN9nl91RW9h9Vn6zSlD5jvDklmQvRk+Lm94W5o+cm3/spGDhh7VG85LMO7qtiAaB0qSSCNAcDXkOtVkdoVnwOW1QBsF3mx2bp8ZIbZt5iIltPx7C4b6xDuMCrXmXfmjOeR+BvMbgRntkp7exox4B1c0D7oaPCZuSPK7wnxV9yuZjwr5bapMQrpBfTLfUfsRcCTELqluELjEJ15CMmNz0JmUFBZbs2Bz19uQS4Qax9Pbw4PQ5Krrsilv6z4asDquVzxrAB5uzQTigwI1kF09w4N2DrRskGyZo0NYZnRwObeKt9rrwdp0jh4d5yk3zBXedqk27TfqzrqEmq99AsHX6XBjrh2+fpflVkWUmJN6Sp7rp7jxuUGGzoSUbnKw7dLba2FF2AWK+d4IVNiAazI1FP2vbzIi6afw03DHXUd1uCBu9WYo1yFyEk36CzaTp6sqru3HWWn8+wVZdV1f27Ky1k0pS+qsRHuZt4N17l4cKzdMYyHs0jIus2BIbn9XC5oFjk8cIENEn+/Juy+zk8DRRC6+qo1cNtoHgDxTsK5vES+cXVtfOv3ByhAsxvyh2kS8azQd0WbC0kBlBBCUT5Uv+dG67ZXUtLTFOIehApWvRwkpp/3bNF/52ClY0YZ9N9BdbQfHBKjOM/c2/Q+gt2QxtOlnX1ZVLpxYyJFu6gEpRnsUoaXGsMUKWhkBB2iC3S63dkspRwVX4Wway0Q0p5xTYUKPsl8ROvXD8qrDzMHYLza66GV8XO7J8cluW5TnJPolwQMvpKk5wELV389RG1dG5slbMPMMBmRfdz2Kl7Ln25GqYCaiwEwO4S3xXlyl7scnLafOj3H19Ar7fw8EifrdvOPnDrw5K7sx943LLZ/l5Oqi8qfIH88QSb5IuLJeGrYj6u90OijnLnw8x3xcOsrC7Ldw+MyIzsSgsV1LuhSI879lxIVW4yML/qjeLHAliGIqGisL3P+2smtyy5F9J2pqsCx0w/OcufnF8rH21F9PscZmpaT7szysCiyuML669nl5rO+kfIPpH5riTMGInYa9fu8XLGs1AQC30GQz2HBLsPZ9j9cJbIKDfJgf0C1IYaukKJW1uRtLWoKTN0Q25JE3lE/ODeTOcmGcmMedKEefCUgRefLFI8cXxxRe+3MS6lC4uNxmu3PT5nvBdYEvTC2yBPLZQFCAx5wFSUrRkb5OSIjMyXY/w8PlpV42y8QazbZEaCr9fyJe5fYE0sH9KA8GZ34ghlYohIt2pLZF/AMELf3N83ijVnfZvL46vbR+kEt/zKAye8Ub5wMAkVtQUG68840rUzFufJCBnB2i4jJeEpHp3S6q/GKkegROOLCRb+hvFIIMTABzj4nAMWypNNCAcg4nK80ck32nulM7KByM00ACRmypFbq52C7nxgYGMfKxjkFHwK7Eqfg+tBMnKOEgGo3PbPgmdw2DBjsGC+nikPhBKPNhQB4S1BCBSgX7xsY9gzlkf7NZH2XF43++imKLXbfcz4H3jYrzXrhCjm9+gYYEGjSUtKWdZ2JJCm3AK14RzAk04Om1H+o1W+NlQ7TMSPh+F8t0sH9Z52kznnTX/ZvwBSmxeg6qlDAUAAAAASUVORK5CYII");background-size:16px 16px;background-repeat:no-repeat;background-position:center center;display:inline-block;width:16px;height:16px;flex-shrink:0;margin-right:4px;content:none;font-size:0;}.live-photo-mute-btn{border-radius:50%;width:32px;height:32px;padding:0;opacity:0.9;display:flex;}.live-photo-mute-btn i{display:block;width:100%;height:100%;font-size:0;content:none;color:transparent;background-repeat:no-repeat;background-position:center center;background-size:18px 18px;}.live-photo-mute-btn[data-muted="true"] i.icon-muted{background-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20width%3D%2724%27%20height%3D%2724%27%3E%3Cpath%20fill%3D%27none%27%20stroke%3D%27white%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%20stroke-width%3D%272%27%20d%3D%27M15%208a5%205%200%200%201%201.912%204.934m-1.377%202.602A5%205%200%200%201%2015%2016m2.7-11a9%209%200%200%201%202.362%2011.086m-1.676%202.299A9%209%200%200%201%2017.7%2019M9.069%205.054L9.5%204.5A.8.8%200%200%201%2011%205v2m0%204v8a.8.8%200%200%201-1.5.5L6%2015H4a1%201%200%200%201-1-1v-4a1%201%200%200%201%201-1h2l1.294-1.664M3%203l18%2018%27%2F%3E%3C%2Fsvg%3E");}.live-photo-mute-btn[data-muted="false"] i.icon-unmuted{background-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20width%3D%2724%27%20height%3D%2724%27%3E%3Cpath%20fill%3D%27none%27%20stroke%3D%27white%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%20stroke-width%3D%272%27%20d%3D%27M15%208a5%205%200%200%201%200%208m2.7-11a9%209%200%200%201%200%2014M6%2015H4a1%201%200%200%201-1-1v-4a1%201%200%200%201%201-1h2l3.5-4.5A.8.8%200%200%201%2011%205v14a.8.8%200%200%201-1.5.5z%27%2F%3E%3C%2Fsvg%3E");}
</style>

使用

下面的 \\ 用于文中转义短代码的渲染,实际使用时请去掉

{{\\< motionPhotos
video="https://s3.dejavu.moe/blog/<Live Photos 视频>.mp4"
image="https://s3.dejavu.moe/blog/<live Photos 照片>.jpg"
alt="我的实况照片" >\\}}

积分

可供参考的资料:

参考资料: