写一个 Live Photo 组件

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

Github: https://github.com/LynanBreeze/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
16
<!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>
);
};

开发 && 构建

开发

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

构建

1
pnpm build