live-photo
v0.0.49
Published
A LivePhoto viewer for web applications
Downloads
83
Maintainers
Readme
中文 | English
✨ 特性
- 🎯 零依赖 - 轻量级实现,无需任何外部依赖
- 📱 跨平台 - 无缝支持移动端(触摸)和桌面端(鼠标)交互
- 🖼️ 智能媒体处理 - 照片和视频之间自动切换,过渡流畅
- 🎨 高度可定制 - 灵活的样式和配置选项,支持图片和视频元素自定义
- 🔄 高级加载 - 支持懒加载和渐进式视频加载,带有视觉反馈
- ⚡ 性能优化 - 高效的资源管理和清理机制
- 🎮 丰富的 API - 完善的公共方法和事件回调,实现完全控制
- 🎭 交互体验 - 长按播放、点击检测、自动播放模式和震动反馈
- 📊 状态管理 - 内置状态跟踪和订阅系统
- 🛡️ 类型安全 - 完整的 TypeScript 支持,提供完整类型定义
- 🎪 框架无关 - 支持原生 JavaScript、Vue、React、Angular 等
📦 安装
npm install live-photo
# 或
pnpm add live-photo
# 或
yarn add live-photo
# 或
bun add live-photo🚀 快速开始
浏览器 (CDN)
<script src="https://fastly.jsdelivr.net/npm/live-photo@latest"></script>
<div id="live-photo-container"></div>
<script>
new LivePhotoViewer({
photoSrc: 'path/to/photo.jpg',
videoSrc: 'path/to/video.mp4',
container: document.getElementById('live-photo-container'),
});
</script>ES 模块
import { LivePhotoViewer } from 'live-photo';
const viewer = new LivePhotoViewer({
photoSrc: 'path/to/photo.jpg',
videoSrc: 'path/to/video.mp4',
container: document.getElementById('live-photo-container'),
});📖 API 参考
配置选项
| 参数 | 类型 | 必填 | 默认值 | 描述 |
| ------------------ | -------------------------- | ---- | ------- | -------------------------------------------------------- |
| photoSrc | string | ✅ | - | 要显示的静态图片 URL |
| videoSrc | string | ✅ | - | 交互时播放的视频 URL |
| container | HTMLElement | ✅ | - | 挂载查看器的 DOM 元素 |
| width | number | string | ❌ | 300px | 查看器宽度(支持 px、%、vh、vw 等) |
| height | number | string | ❌ | 300px | 查看器高度(支持 px、%、vh、vw 等) |
| autoplay | boolean | ❌ | true | 启用悬停(桌面)或长按(移动端)时自动播放视频 |
| lazyLoadVideo | boolean | ❌ | false | 延迟视频加载,直到查看器进入视口 |
| longPressDelay | number | ❌ | 300 | 区分点击和长按的时间阈值(毫秒) |
| borderRadius | number | string | ❌ | - | 容器的边框圆角(支持 px、%、rem 等) |
| theme | 'light' | 'dark' | 'auto'| ❌ | - | UI 元素的颜色主题 |
| preload | 'auto' | 'metadata' | 'none' | ❌ | - | 视频预加载策略 |
| retryAttempts | number | ❌ | 3 | 视频加载失败时的重试次数 |
| enableVibration | boolean | ❌ | true | 在支持的设备上启用震动反馈 |
| staticBadgeIcon | boolean | ❌ | false | 保持徽章图标静态(无斜杠),不随 autoplay 状态变化 |
| imageCustomization | ElementCustomization | ❌ | - | 图片元素的自定义属性和样式 |
| videoCustomization | ElementCustomization | ❌ | - | 视频元素的自定义属性和样式 |
ElementCustomization 接口
interface ElementCustomization {
attributes?: Record<string, string>; // HTML 属性(如 { alt: "...", loading: "lazy" })
styles?: Partial<CSSStyleDeclaration>; // CSS 样式(如 { objectFit: "cover" })
}事件回调
所有回调现在都会返回原始事件对象和相关元素,让您可以访问完整的事件信息和元素属性。
| 回调 | 参数 | 描述 |
| -------------- | ------------------------ | --------------------------------------- |
| onPhotoLoad | (event, photo) => void | 图片加载完成时触发,返回事件对象和图片元素 |
| onVideoLoad | (duration, event, video) => void | 视频元数据加载完成时触发,返回视频时长(秒)、事件对象和视频元素 |
| onCanPlay | (event, video) => void | 视频准备好播放时触发,返回事件对象和视频元素 |
| onLoadStart | () => void | 视频开始加载时触发(懒加载模式) |
| onLoadProgress | (loaded, total) => void | 视频下载进度时触发 |
| onProgress | (progress, event, video) => void | 视频缓冲进度时触发(0-100),返回进度、事件对象和视频元素 |
| onEnded | (event, video) => void | 视频播放完成时触发,返回事件对象和视频元素 |
| onClick | (event) => void | 短按/点击时触发,返回事件对象 |
| onError | (error, event?) => void | 发生错误时触发,返回错误对象和可选的事件对象 |
LivePhotoError 接口
interface LivePhotoError {
type: 'VIDEO_LOAD_ERROR' | 'PHOTO_LOAD_ERROR' | 'PLAYBACK_ERROR' | 'VALIDATION_ERROR';
message: string;
originalError?: Error;
}公共方法
所有方法都可在 LivePhotoViewer 实例上使用:
| 方法 | 返回值 | 描述 |
| ---------------- | --------------- | --------------------------------------- |
| play() | Promise<void> | 开始或恢复视频播放 |
| pause() | void | 暂停视频播放 |
| stop() | void | 停止视频并重置到开始位置 |
| toggle() | void | 切换播放和暂停状态 |
| getState() | LivePhotoState| 获取当前查看器状态(只读) |
| destroy() | void | 清理资源并从 DOM 中移除查看器 |
LivePhotoState 接口
interface LivePhotoState {
isPlaying: boolean; // 视频是否正在播放
autoplay: boolean; // 当前的自动播放设置
videoError: boolean; // 视频加载是否失败
videoLoaded: boolean; // 视频是否已加载
aspectRatio: number; // 计算出的照片纵横比
isLongPressPlaying: boolean; // 是否因长按而播放
}🎯 工作原理
桌面端交互
- 悬停在徽章上: 悬停在 LIVE 徽章上时自动播放视频(如果启用了自动播放)
- 移开鼠标: 视频停止并返回照片
- 点击徽章: 打开下拉菜单以切换自动播放设置
移动端交互
- 长按: 按住照片播放视频
- 释放: 视频停止并返回照片
- 短按: 触发
onClick回调而不播放视频 - 触觉反馈: 在支持的设备上提供震动反馈(如果启用)
加载行为
- 标准加载: 视频随组件立即加载
- 懒加载: 仅当查看器进入视口时才加载视频
- 进度指示器: 视觉反馈在 LIVE 徽章中显示加载进度
- 错误恢复: 失败加载的自动重试机制
🎨 自定义
样式配置
const viewer = new LivePhotoViewer({
photoSrc: "photo.jpg",
videoSrc: "video.mp4",
container: document.getElementById("container"),
// 容器样式
width: "100%",
height: "auto",
borderRadius: "16px",
theme: "dark",
// 图片自定义
imageCustomization: {
styles: {
objectFit: "cover",
filter: "brightness(1.1)",
},
attributes: {
alt: "我的实时照片",
loading: "lazy",
draggable: "false",
},
},
// 视频自定义
videoCustomization: {
styles: {
objectFit: "contain",
filter: "contrast(1.1)",
},
attributes: {
preload: "metadata",
},
},
});自定义 CSS
可以使用 CSS 覆盖默认样式:
/* 容器 */
.live-photo-container {
border: 2px solid #4F46E5;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* 徽章 */
.live-photo-badge {
background: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px);
}
/* 播放状态 */
.live-photo-container.playing {
transform: scale(1.02);
transition: transform 0.3s ease;
}
/* 下拉菜单 */
.dropdown-menu {
background: rgba(255, 255, 255, 0.95);
}🔧 浏览器支持
- ✅ Chrome (最新版)
- ✅ Firefox (最新版)
- ✅ Safari (最新版)
- ✅ Edge (最新版)
- ✅ 移动浏览器 (iOS Safari, Chrome Mobile)
要求
- 支持 ES6+ 的现代浏览器
- 支持
IntersectionObserver(用于懒加载) - 支持
Promise和async/await
📋 最佳实践
优化媒体文件
- 使用压缩的图片格式(JPEG、WebP)
- 使用短视频片段(建议 2-3 秒)
- 考虑使用自适应比特率视频以获得更好的性能
使用懒加载
- 为首屏以下的内容启用
lazyLoadVideo: true - 提高初始页面加载性能
- 为首屏以下的内容启用
优雅地处理错误
- 始终实现
onError回调 - 为加载失败提供备用 UI
- 始终实现
清理资源
- 移除查看器时调用
destroy()方法 - 在 SPA(单页应用)中特别重要
- 移除查看器时调用
响应式设计
- 使用相对单位(
%、vh、vw)实现响应式尺寸 - 为您的宽高比设置适当的
objectFit值
- 使用相对单位(
🐛 故障排除
移动端视频无法播放
- 确保视频具有
muted属性(组件自动设置) - 检查视频格式是否受支持(推荐 MP4 H.264)
- 验证是否设置了
playsInline(组件自动设置)
视频无法加载
- 检查视频 URL 是否可访问且启用了 CORS
- 验证视频文件未损坏
- 检查浏览器控制台的具体错误
- 尝试增加
retryAttempts选项
性能问题
- 为一个页面上的多个查看器启用
lazyLoadVideo - 优化视频文件大小和格式
- 考虑使用更短的视频片段
自动播放不工作
- 验证选项中设置了
autoplay: true - 检查视频是否静音(浏览器要求自动播放必须静音)
- 桌面端:确保您悬停在徽章上
- 移动端:改用长按(自动播放工作方式不同)
🔧 开发
# 安装依赖
pnpm install
# 开发模式(监听)
pnpm dev
# 生产构建
pnpm build
# 运行 playground
cd playground
pnpm dev📄 许可证
MIT License - 详见 LICENSE 文件
🤝 贡献
欢迎贡献!请随时提交 Pull Request。
- Fork 仓库
- 创建您的特性分支 (
git checkout -b feature/amazing-feature) - 提交您的更改 (
git commit -m 'Add some amazing feature') - 推送到分支 (
git push origin feature/amazing-feature) - 打开 Pull Request
💖 支持
如果您觉得这个项目有帮助,请考虑:
- ⭐ 给仓库点星
- 🐛 报告 bug
- 💡 提出新功能建议
- 📖 改进文档
📬 联系方式
- 作者: Icey Wu
- 邮箱: [email protected]
- GitHub: @IceyWu
🙏 致谢
灵感来自 Apple 在 iOS 设备上的 Live Photos 功能。
用 ❤️ 制作,作者 Icey Wu
� 使用示例
原生 JavaScript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Photo 演示</title>
<script src="https://fastly.jsdelivr.net/npm/live-photo@latest"></script>
</head>
<body>
<div id="live-photo-container"></div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const container = document.getElementById("live-photo-container");
const viewer = new LivePhotoViewer({
photoSrc: "https://example.com/photo.jpg",
videoSrc: "https://example.com/video.mp4",
container: container,
width: 400,
height: 600,
borderRadius: "12px",
autoplay: true,
lazyLoadVideo: true,
enableVibration: true,
imageCustomization: {
styles: {
objectFit: "cover",
},
attributes: {
alt: "美丽的实时照片",
loading: "lazy",
},
},
videoCustomization: {
styles: {
objectFit: "cover",
},
},
// 事件回调
onPhotoLoad: (event, photo) => {
console.log("照片已加载", photo.naturalWidth, "x", photo.naturalHeight);
},
onVideoLoad: (duration, event, video) => {
console.log(`视频已加载,时长: ${duration}秒`);
},
onProgress: (progress, event, video) => {
console.log(`加载中: ${progress}%`);
},
onError: (error, event) => console.error("错误:", error),
onClick: (event) => console.log("已点击!"),
});
// 通过编程方式控制播放
// viewer.play();
// viewer.pause();
// viewer.stop();
// viewer.toggle();
// 获取当前状态
// const state = viewer.getState();
// console.log(state.isPlaying, state.autoplay);
});
</script>
</body>
</html>Vue 3 (组合式 API)
<template>
<div ref="containerRef"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { LivePhotoViewer } from "live-photo";
import type { LivePhotoAPI } from "live-photo";
const containerRef = ref<HTMLElement | null>(null);
const viewerInstance = ref<LivePhotoAPI | null>(null);
onMounted(() => {
if (containerRef.value) {
viewerInstance.value = new LivePhotoViewer({
photoSrc: "https://example.com/photo.jpg",
videoSrc: "https://example.com/video.mp4",
container: containerRef.value,
width: 400,
height: 600,
borderRadius: "12px",
autoplay: true,
lazyLoadVideo: true,
enableVibration: true,
imageCustomization: {
styles: {
objectFit: "cover",
},
attributes: {
alt: "美丽的实时照片",
loading: "lazy",
},
},
videoCustomization: {
styles: {
objectFit: "cover",
},
},
onPhotoLoad: (event, photo) => {
console.log("照片已加载", photo.naturalWidth, "x", photo.naturalHeight);
},
onVideoLoad: (duration, event, video) => {
console.log(`视频已加载,时长: ${duration}秒`);
},
onProgress: (progress, event, video) => {
console.log(`加载中: ${progress}%`);
},
onError: (error, event) => console.error("错误:", error),
onClick: (event) => console.log("已点击!"),
});
}
});
// 组件卸载时清理
onUnmounted(() => {
if (viewerInstance.value) {
viewerInstance.value.destroy();
}
});
// 示例:控制方法
const play = () => viewerInstance.value?.play();
const pause = () => viewerInstance.value?.pause();
const toggle = () => viewerInstance.value?.toggle();
</script>React (TypeScript)
import React, { useEffect, useRef } from "react";
import { LivePhotoViewer } from "live-photo";
import type { LivePhotoAPI } from "live-photo";
const LivePhotoComponent: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const viewerRef = useRef<LivePhotoAPI | null>(null);
useEffect(() => {
if (containerRef.current) {
viewerRef.current = new LivePhotoViewer({
photoSrc: "https://example.com/photo.jpg",
videoSrc: "https://example.com/video.mp4",
container: containerRef.current,
width: 400,
height: 600,
borderRadius: "12px",
autoplay: true,
lazyLoadVideo: true,
enableVibration: true,
imageCustomization: {
styles: {
objectFit: "cover",
},
attributes: {
alt: "美丽的实时照片",
loading: "lazy",
},
},
videoCustomization: {
styles: {
objectFit: "cover",
},
},
onPhotoLoad: (event, photo) => {
console.log("照片已加载", photo.naturalWidth, "x", photo.naturalHeight);
},
onVideoLoad: (duration, event, video) => {
console.log(`视频已加载,时长: ${duration}秒`);
},
onProgress: (progress, event, video) => {
console.log(`加载中: ${progress}%`);
},
onError: (error, event) => console.error("错误:", error),
onClick: (event) => console.log("已点击!"),
});
}
// 卸载时清理
return () => {
if (viewerRef.current) {
viewerRef.current.destroy();
}
};
}, []);
// 示例:控制方法
const handlePlay = () => viewerRef.current?.play();
const handlePause = () => viewerRef.current?.pause();
const handleToggle = () => viewerRef.current?.toggle();
return (
<div>
<div ref={containerRef}></div>
<div>
<button onClick={handlePlay}>播放</button>
<button onClick={handlePause}>暂停</button>
<button onClick={handleToggle}>切换</button>
</div>
</div>
);
};
export default LivePhotoComponent;高级用法
访问回调参数
所有回调现在都提供原始事件对象和相关元素,让您可以访问完整的DOM信息:
const viewer = new LivePhotoViewer({
photoSrc: "photo.jpg",
videoSrc: "video.mp4",
container: document.getElementById("container"),
onVideoLoad: (duration, event, video) => {
// duration: 视频时长(秒),是HTML5 Video API的标准单位
console.log(`视频时长: ${duration}秒`);
// event: 原始DOM事件对象
console.log("事件类型:", event.type); // "loadedmetadata"
console.log("是否可信:", event.isTrusted);
// video: HTMLVideoElement 元素
console.log("视频尺寸:", video.videoWidth, "x", video.videoHeight);
console.log("当前时间:", video.currentTime);
console.log("就绪状态:", video.readyState);
},
onProgress: (progress, event, video) => {
// progress: 加载进度百分比 (0-100)
console.log(`进度: ${progress}%`);
// 通过video元素访问更多信息
if (video.buffered.length > 0) {
const bufferedEnd = video.buffered.end(0);
console.log(`已缓冲: ${bufferedEnd}秒`);
}
},
onPhotoLoad: (event, photo) => {
// photo: HTMLImageElement 元素
console.log("图片原始尺寸:", photo.naturalWidth, "x", photo.naturalHeight);
console.log("图片显示尺寸:", photo.width, "x", photo.height);
console.log("图片源:", photo.src);
},
onError: (error, event) => {
console.log("错误类型:", error.type);
console.log("错误信息:", error.message);
// event 是可选的,某些错误可能没有关联的事件
if (event) {
console.log("错误事件:", event);
}
},
});使用 Intersection Observer 懒加载
const viewer = new LivePhotoViewer({
photoSrc: "photo.jpg",
videoSrc: "video.mp4",
container: document.getElementById("container"),
lazyLoadVideo: true, // 仅当查看器进入视口时才加载视频
onLoadStart: () => {
console.log("视频开始加载");
},
onProgress: (progress, event, video) => {
console.log(`视频缓冲中: ${progress}%`);
console.log(`已缓冲: ${video.buffered.length > 0 ? video.buffered.end(0) : 0}秒`);
},
});自定义错误处理
const viewer = new LivePhotoViewer({
photoSrc: "photo.jpg",
videoSrc: "video.mp4",
container: document.getElementById("container"),
retryAttempts: 5, // 失败时重试 5 次
onError: (error, event) => {
console.log("原始事件:", event);
switch (error.type) {
case 'VIDEO_LOAD_ERROR':
console.error("视频加载失败:", error.message);
// 显示备用 UI
break;
case 'PHOTO_LOAD_ERROR':
console.error("照片加载失败:", error.message);
break;
case 'PLAYBACK_ERROR':
console.error("播放失败:", error.message);
break;
}
},
});状态订阅
const viewer = new LivePhotoViewer({
photoSrc: "photo.jpg",
videoSrc: "video.mp4",
container: document.getElementById("container"),
});
// 获取当前状态
const state = viewer.getState();
console.log(state.isPlaying); // false
console.log(state.autoplay); // true
// 注意:对于响应式状态更新,可以轮询 getState()
// 或使用事件回调响应式尺寸
const viewer = new LivePhotoViewer({
photoSrc: "photo.jpg",
videoSrc: "video.mp4",
container: document.getElementById("container"),
width: "100%", // 响应式宽度
height: "50vh", // 视口高度的 50%
borderRadius: "1rem",
imageCustomization: {
styles: {
objectFit: "cover", // 保持宽高比
},
},
});