写一个 Live Photo 组件

一个简单的 Live Photo 组件实现。

预览

我的 Live Photo

html代码
1
<iframe src="/static/live-photo/?picUrl=https://r2-assets-cn.thelynan.com/u/A001_07251216_C317-FfGABM.jpg&videoUrl=https://r2-assets-cn.thelynan.com/u/A001_07251216_C317-DQ6Nj6.mp4" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="width: 100%; aspect-ratio: 16/9;"></iframe>

Apple Live Photo

如果你有开通 iCloud + 服务,在网页版 iCloud Photos 中也能预览 Live Photo。

html代码
1
<iframe src="/static/live-photo/?picUrl=/static/live-photo/test/live.jpg&videoUrl=/static/live-photo/test/live.mp4&useApple=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="width: 100%; aspect-ratio: 16/9;"></iframe>

对比

Apple Live Photo 通过 Javascript 加载照片和视频文件,解析成帧,用 SDK 中的逻辑控制播放,这就无形中需要面对 跨域 的问题。

我的Apple
更好的兼容性在 iOS 微信中异常
声音、静音可选静音
更丰富的播放器效果选项
点击触发点击触发(桌面端), 长按触发(移动端)
个性化的图标和相册里的一样

参数

参数名称类型
photoSrcstring, 必须
videoSrcstring, 必须
loopboolean
mutedboolean
useAppleboolean

原理

Live Photo并不是一种神秘的文件格式,而是一张照片和一段视频的组合。

所以需要展示 Live Photo 只需添加一个切换逻辑即可。

Apple SDK

Apple 为开发者提供了官方的 JS SDK。请查看 https://developer.apple.com/documentation/livephotoskitjs

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://cdn.apple-livephotoskit.com/lpk/1/livephotoskit.js"></script>
</head>
<body>
<div
data-live-photo
data-photo-src="https://..."
data-video-src="https://..."
style="width: 320px; height: 320px"
></div>
</body>
</html>

如果要在 React 中实现,很简单,如下:

index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useRef, useEffect } from "react";
import * as LivePhotosKit from "livephotoskit";

const LivePhotosKitReact = ({ className, photoSrc, videoSrc }) => {
const nodeRef = useRef(null);

useEffect(() => {
const player = LivePhotosKit.Player(nodeRef.current);
player.photoSrc = photoSrc;
player.videoSrc = videoSrc;
}, []);

return <div ref={nodeRef} className={className}></div>;
};

My Style

我们所需要的只是在 <img><video> 之间切换。

index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { useRef, useState } from "react";

const LivePhoto = (props) => {
const { photoSrc, videoSrc, muted, loop, useApple } = props;
const [imageReady, setImageReady] = useState(false);
const [videoPlaying, setVideoPlaying] = useState(false);
const [videoRunning, setVideoRunning] = useState(false);
const [videoReady, setVideoReady] = useState(false);
const videoRef = useRef(null);

const playVideo = () => {
if (videoRunning) {
videoRef.current.pause();
} else {
setVideoPlaying(true);
videoRef.current.play();
}
};

const onImageLoad = () => {
setImageReady(true);
// 解决 onCanPlay and onLoadedMetadata 在 iOS 端微信内不会被触发的 Hack
if (
/iphone/i.test(navigator.userAgent) &&
/micromessenger/i.test(navigator.userAgent)
) {
setTimeout(() => {
setVideoReady(true);
}, 500);
}
};

return (
<div className='live-photo'>
{useApple ? (
<LivePhotosKitReact
className='live-img'
photoSrc={photoSrc}
videoSrc={videoSrc}
/>
) : (
<>
<div
className='live-trigger'
onClick={playVideo}
style={{ opacity: Number(videoReady) }}
>
<div
className='trigger-icon'
style={{
animationPlayState: videoRunning ? "running" : "paused",
}}
></div>
<span className='trigger-text'>LIVE</span>
</div>
<img
className='live-img'
src={photoSrc}
onLoad={onImageLoad}
style={{ opacity: Number(imageReady) }}
/>
<video
playsInline
webkit-playsinline
className='live-video'
loop={loop}
muted={muted}
ref={videoRef}
src={videoSrc}
style={{ opacity: Number(videoPlaying) }}
onCanPlay={() => setVideoReady(true)}
onLoadedMetadata={() => setVideoReady(true)}
onPlaying={() => setVideoRunning(true)}
onPause={() => setVideoRunning(false)}
onEnded={() => setVideoPlaying(false)}
></video>
</>
)}
</div>
);
};

开发 & 构建

开发

LynanBreeze/live-photo: A simple live-photo React component.
1
git clone git@github.com:LynanBreeze/live-photo.git
1
pnpm install && pnpm run dev

构建

1
pnpm build