{"id":28339355,"url":"https://github.com/iceywu/live-photo","last_synced_at":"2026-02-14T05:32:34.423Z","repository":{"id":269687444,"uuid":"908151734","full_name":"IceyWu/live-photo","owner":"IceyWu","description":"A LivePhoto viewer for web applications🖼️","archived":false,"fork":false,"pushed_at":"2026-02-05T03:24:42.000Z","size":293,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-05T13:59:07.025Z","etag":null,"topics":["angular","component","image","livephoto","react","vue"],"latest_commit_sha":null,"homepage":"https://live-photo.netlify.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/IceyWu.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-12-25T09:24:42.000Z","updated_at":"2026-02-05T03:20:15.000Z","dependencies_parsed_at":"2025-10-25T07:14:06.971Z","dependency_job_id":"0bd4b28e-a59b-476d-a544-503e7c12e067","html_url":"https://github.com/IceyWu/live-photo","commit_stats":null,"previous_names":["iceywu/live-photo"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/IceyWu/live-photo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IceyWu%2Flive-photo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IceyWu%2Flive-photo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IceyWu%2Flive-photo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IceyWu%2Flive-photo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/IceyWu","download_url":"https://codeload.github.com/IceyWu/live-photo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IceyWu%2Flive-photo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29438444,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T05:24:35.651Z","status":"ssl_error","status_checked_at":"2026-02-14T05:24:34.830Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","component","image","livephoto","react","vue"],"created_at":"2025-05-27T01:24:19.700Z","updated_at":"2026-02-14T05:32:34.387Z","avatar_url":"https://github.com/IceyWu.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e\n  \u003cbr\u003e\n  \u003csvg width=\"120\" height=\"120\" viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\u003e\n    \u003crect width=\"120\" height=\"120\" rx=\"16\" fill=\"#4F46E5\"/\u003e\n    \u003cpath d=\"M85 45H73L70.2 40.4C69.9331 39.9011 69.5539 39.4752 69.0953 39.1581C68.6367 38.841 68.1119 38.6417 67.57 38.575C67.3808 38.5461 67.1905 38.5273 67 38.519H53C52.8095 38.5273 52.6192 38.5461 52.43 38.575C51.8881 38.6417 51.3633 38.841 50.9047 39.1581C50.4461 39.4752 50.0669 39.9011 49.8 40.4L47 45H35C33.6739 45 32.4021 45.5268 31.4645 46.4645C30.5268 47.4021 30 48.6739 30 50V80C30 81.3261 30.5268 82.5979 31.4645 83.5355C32.4021 84.4732 33.6739 85 35 85H85C86.3261 85 87.5979 84.4732 88.5355 83.5355C89.4732 82.5979 90 81.3261 90 80V50C90 48.6739 89.4732 47.4021 88.5355 46.4645C87.5979 45.5268 86.3261 45 85 45ZM60 77.5C57.0333 77.5 54.1332 76.7082 51.6665 75.2248C49.1997 73.7414 47.2771 71.6277 46.1418 69.1385C45.0065 66.6493 44.7094 63.8916 45.2882 61.2295C45.8669 58.5673 47.2956 56.1307 49.3934 54.2582C51.4912 52.3857 54.1939 51.1055 57.1477 50.5843C60.1015 50.0631 63.1599 50.3289 65.9107 51.3503C68.6615 52.3717 70.9927 54.1022 72.6265 56.3265C74.2604 58.5507 75.1111 61.1701 75.1111 63.8333C75.1111 67.4674 73.4493 70.9534 70.4877 73.5702C67.5261 76.187 63.5768 77.6667 59.4444 77.6667L60 77.5Z\" fill=\"white\"/\u003e\n    \u003ccircle cx=\"60\" cy=\"64\" r=\"12\" fill=\"#4F46E5\" stroke=\"white\" stroke-width=\"3\"/\u003e\n  \u003c/svg\u003e\n  \u003cbr\u003e\n  live-photo\n  \u003cbr\u003e\n\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e🚀 \u003cstrong\u003elive-photo\u003c/strong\u003e — A tiny, zero-dependency Live Photo web viewer that works with any modern front-end framework (Vue, React, Angular, Svelte) or plain JavaScript. Bring iOS‑style Live Photos to the web with minimal code.\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://www.npmjs.com/package/live-photo\" target=\"__blank\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/live-photo?color=a1b858\u0026label=\" alt=\"NPM version\"\u003e\u003c/a\u003e\n\u003ca href=\"https://www.npmjs.com/package/live-photo\" target=\"__blank\"\u003e\u003cimg alt=\"NPM Downloads\" src=\"https://img.shields.io/npm/dm/live-photo?color=50a36f\u0026label=\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\nLive demo: \u003ca href=\"https://live-photo.netlify.app\" target=\"_blank\"\u003ehttps://live-photo.netlify.app\u003c/a\u003e\n\u003c/p\u003e\n\n**English** | [中文](./README.zh-CN.md)\n\n## ✨ Features\n\n- 🎯 **Zero Dependencies** - Lightweight implementation with no external dependencies\n- 📱 **Cross-Platform** - Seamless support for both mobile (touch) and desktop (mouse) interactions\n- 🖼️ **Smart Media Handling** - Automatic switching between photo and video with smooth transitions\n- 🎨 **Highly Customizable** - Flexible styling and configuration options for both image and video elements\n- 🔄 **Advanced Loading** - Support for lazy loading and progressive video loading with visual feedback\n- ⚡ **Performance Optimized** - Efficient resource management and clean-up mechanisms\n- 🎮 **Rich API** - Comprehensive public methods and event callbacks for full control\n- 🎭 **Interactive Experience** - Long-press to play, click detection, auto-play modes, and haptic feedback\n- � **State Management** - Built-in state tracking and subscription system\n- 🛡️ **Type Safe** - Full TypeScript support with complete type definitions\n- 🎪 **Framework Agnostic** - Works with vanilla JavaScript, Vue, React, Angular, and more\n\n## 📦 Installation\n\n```bash\nnpm install live-photo\n# or\npnpm add live-photo\n# or\nyarn add live-photo\n# or\nbun add live-photo\n```\n\n## 🛠️ Utility Functions\n\n### livePhotoExtract\n\nExtract photo and video from Live Photo files (HEIC/MOV combined format).\n\n```javascript\nimport { extractFromLivePhoto } from 'live-photo';\n\n// Extract from file input\nconst fileInput = document.querySelector('input[type=\"file\"]');\nfileInput.addEventListener('change', async (e) =\u003e {\n  const file = e.target.files[0];\n  const result = await extractFromLivePhoto(file);\n  \n  if (result) {\n    const { photoBlob, photoUrl, videoBlob, videoUrl } = result;\n    \n    // Use extracted photo and video\n    const viewer = new LivePhotoViewer({\n      photoSrc: photoUrl,\n      videoSrc: videoUrl,\n      container: document.getElementById('container'),\n    });\n    \n    // Clean up URLs when done\n    // URL.revokeObjectURL(photoUrl);\n    // URL.revokeObjectURL(videoUrl);\n  }\n});\n```\n\n**Returns:**\n```typescript\ninterface ExtractResult {\n  photoBlob: Blob;   // JPEG image blob\n  photoUrl: string;  // Object URL for the photo\n  videoBlob: Blob;   // MP4 video blob\n  videoUrl: string;  // Object URL for the video\n}\n```\n\n**Note:** Remember to revoke object URLs when they're no longer needed to prevent memory leaks.\n\n## 🚀 Quick Start\n\n### Browser (CDN)\n\n```html\n\u003cscript src=\"https://fastly.jsdelivr.net/npm/live-photo@latest\"\u003e\u003c/script\u003e\n\n\u003cdiv id=\"live-photo-container\"\u003e\u003c/div\u003e\n\n\u003cscript\u003e\n  new LivePhotoViewer({\n    photoSrc: 'path/to/photo.jpg',\n    videoSrc: 'path/to/video.mp4',\n    container: document.getElementById('live-photo-container'),\n  });\n\u003c/script\u003e\n```\n\n### ES Module\n\n```javascript\nimport { LivePhotoViewer } from 'live-photo';\n\nconst viewer = new LivePhotoViewer({\n  photoSrc: 'path/to/photo.jpg',\n  videoSrc: 'path/to/video.mp4',\n  container: document.getElementById('live-photo-container'),\n});\n```\n\n## 📖 API Reference\n\n### Configuration Options\n\n| Parameter          | Type                       | Required | Default | Description                                                              |\n| ------------------ | -------------------------- | -------- | ------- | ------------------------------------------------------------------------ |\n| photoSrc           | string                     | ✅       | -       | URL of the static image to display                                      |\n| videoSrc           | string                     | ✅       | -       | URL of the video to play on interaction                                 |\n| container          | HTMLElement                | ✅       | -       | DOM element to mount the viewer                                          |\n| width              | number \\| string           | ❌       | `300px` | Width of the viewer (supports px, %, vh, vw, etc.)                       |\n| height             | number \\| string           | ❌       | `300px` | Height of the viewer (supports px, %, vh, vw, etc.)                      |\n| autoplay           | boolean                    | ❌       | `true`  | Enable automatic video playback on hover (desktop) or long-press (mobile)|\n| lazyLoadVideo      | boolean                    | ❌       | `false` | Delay video loading until viewer is in viewport                         |\n| longPressDelay     | number                     | ❌       | `300`   | Time threshold (ms) to distinguish between click and long-press          |\n| borderRadius       | number \\| string           | ❌       | -       | Border radius for the container (supports px, %, rem, etc.)              |\n| theme              | 'light' \\| 'dark' \\| 'auto'| ❌       | -       | Color theme for UI elements                                              |\n| preload            | 'auto' \\| 'metadata' \\| 'none' | ❌  | -       | Video preload strategy                                                   |\n| retryAttempts      | number                     | ❌       | `3`     | Number of retry attempts for failed video loads                          |\n| enableVibration    | boolean                    | ❌       | `true`  | Enable haptic feedback on supported devices                              |\n| staticBadgeIcon    | boolean                    | ❌       | `false` | Keep badge icon static (no slash) regardless of autoplay state           |\n| imageCustomization | ElementCustomization       | ❌       | -       | Custom attributes and styles for the image element                       |\n| videoCustomization | ElementCustomization       | ❌       | -       | Custom attributes and styles for the video element                       |\n\n### ElementCustomization Interface\n\n```typescript\ninterface ElementCustomization {\n  attributes?: Record\u003cstring, string\u003e;  // HTML attributes (e.g., { alt: \"...\", loading: \"lazy\" })\n  styles?: Partial\u003cCSSStyleDeclaration\u003e; // CSS styles (e.g., { objectFit: \"cover\" })\n}\n```\n\n### Event Callbacks\n\nAll callbacks now return the original event object and related elements, giving you access to complete event information and element properties.\n\n| Callback       | Parameters               | Description                                          |\n| -------------- | ------------------------ | ---------------------------------------------------- |\n| onPhotoLoad    | `(event, photo) =\u003e void`             | Triggered when the photo finishes loading, returns event object and image element            |\n| onVideoLoad    | `(duration, event, video) =\u003e void`   | Triggered when video metadata is loaded, returns duration (seconds), event object and video element            |\n| onCanPlay      | `(event, video) =\u003e void`             | Triggered when the video is ready to play, returns event object and video element            |\n| onLoadStart    | `() =\u003e void`                         | Triggered when video loading starts (lazy load mode) |\n| onLoadProgress | `(loaded, total) =\u003e void`            | Triggered during video download progress             |\n| onProgress     | `(progress, event, video) =\u003e void`   | Triggered with video buffering progress (0-100), returns progress, event object and video element      |\n| onEnded        | `(event, video) =\u003e void`             | Triggered when video playback completes, returns event object and video element              |\n| onClick        | `(event) =\u003e void`                    | Triggered on short press/click, returns event object                       |\n| onError        | `(error, event?) =\u003e void`            | Triggered when an error occurs, returns error object and optional event object                       |\n\n### LivePhotoError Interface\n\n```typescript\ninterface LivePhotoError {\n  type: 'VIDEO_LOAD_ERROR' | 'PHOTO_LOAD_ERROR' | 'PLAYBACK_ERROR' | 'VALIDATION_ERROR';\n  message: string;\n  originalError?: Error;\n}\n```\n\n### Public Methods\n\nAll methods are available on the `LivePhotoViewer` instance:\n\n| Method           | Returns         | Description                                          |\n| ---------------- | --------------- | ---------------------------------------------------- |\n| `play()`         | `Promise\u003cvoid\u003e` | Start or resume video playback                       |\n| `pause()`        | `void`          | Pause video playback                                 |\n| `stop()`         | `void`          | Stop video and reset to beginning                    |\n| `toggle()`       | `void`          | Toggle between play and pause states                 |\n| `getState()`     | `LivePhotoState`| Get current viewer state (readonly)                  |\n| `destroy()`      | `void`          | Clean up resources and remove viewer from DOM        |\n\n### LivePhotoState Interface\n\n```typescript\ninterface LivePhotoState {\n  isPlaying: boolean;           // Whether video is currently playing\n  autoplay: boolean;            // Current autoplay setting\n  videoError: boolean;          // Whether video loading failed\n  videoLoaded: boolean;         // Whether video has been loaded\n  aspectRatio: number;          // Calculated aspect ratio of the photo\n  isLongPressPlaying: boolean;  // Whether playing due to long-press\n}\n```\n\n## 🎯 How It Works\n\n### Desktop Interaction\n- **Hover on Badge**: Video plays automatically when hovering over the LIVE badge (if autoplay is enabled)\n- **Hover Off**: Video stops and returns to photo\n- **Click Badge**: Opens dropdown menu to toggle autoplay settings\n\n### Mobile Interaction\n- **Long Press**: Hold down on the photo to play the video\n- **Release**: Video stops and returns to photo\n- **Short Tap**: Triggers `onClick` callback without playing video\n- **Haptic Feedback**: Vibration feedback on supported devices (if enabled)\n\n### Loading Behavior\n- **Standard Loading**: Video loads immediately with the component\n- **Lazy Loading**: Video loads only when the viewer enters the viewport\n- **Progress Indicator**: Visual feedback shows loading progress in the LIVE badge\n- **Error Recovery**: Automatic retry mechanism for failed loads\n\n## 🎨 Customization\n\n### Styling\n\n```javascript\nconst viewer = new LivePhotoViewer({\n  photoSrc: \"photo.jpg\",\n  videoSrc: \"video.mp4\",\n  container: document.getElementById(\"container\"),\n  \n  // Container styling\n  width: \"100%\",\n  height: \"auto\",\n  borderRadius: \"16px\",\n  theme: \"dark\",\n  \n  // Image customization\n  imageCustomization: {\n    styles: {\n      objectFit: \"cover\",\n      filter: \"brightness(1.1)\",\n    },\n    attributes: {\n      alt: \"My Live Photo\",\n      loading: \"lazy\",\n      draggable: \"false\",\n    },\n  },\n  \n  // Video customization\n  videoCustomization: {\n    styles: {\n      objectFit: \"contain\",\n      filter: \"contrast(1.1)\",\n    },\n    attributes: {\n      preload: \"metadata\",\n    },\n  },\n});\n```\n\n### Custom CSS\n\nYou can override the default styles using CSS:\n\n```css\n/* Container */\n.live-photo-container {\n  border: 2px solid #4F46E5;\n  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n\n/* Badge */\n.live-photo-badge {\n  background: rgba(0, 0, 0, 0.8) !important;\n  backdrop-filter: blur(10px);\n}\n\n/* Playing state */\n.live-photo-container.playing {\n  transform: scale(1.02);\n  transition: transform 0.3s ease;\n}\n\n/* Dropdown menu */\n.dropdown-menu {\n  background: rgba(255, 255, 255, 0.95);\n}\n```\n\n## 🔧 Browser Support\n\n- ✅ Chrome (latest)\n- ✅ Firefox (latest)\n- ✅ Safari (latest)\n- ✅ Edge (latest)\n- ✅ Mobile browsers (iOS Safari, Chrome Mobile)\n\n### Requirements\n- Modern browser with ES6+ support\n- Support for `IntersectionObserver` (for lazy loading)\n- Support for `Promise` and `async/await`\n\n## 📋 Best Practices\n\n1. **Optimize Media Files**\n   - Use compressed images (JPEG, WebP)\n   - Use short video clips (2-3 seconds recommended)\n   - Consider using adaptive bitrate videos for better performance\n\n2. **Use Lazy Loading**\n   - Enable `lazyLoadVideo: true` for content below the fold\n   - Improves initial page load performance\n\n3. **Handle Errors Gracefully**\n   - Always implement `onError` callback\n   - Provide fallback UI for failed loads\n\n4. **Clean Up Resources**\n   - Call `destroy()` method when removing the viewer\n   - Especially important in SPAs (Single Page Applications)\n\n5. **Responsive Design**\n   - Use relative units (`%`, `vh`, `vw`) for responsive sizing\n   - Set appropriate `objectFit` values for your aspect ratios\n\n## 🐛 Troubleshooting\n\n### Video not playing on mobile\n- Ensure video has `muted` attribute (automatically set by the component)\n- Check that video format is supported (MP4 H.264 recommended)\n- Verify that `playsInline` is set (automatically set by the component)\n\n### Video not loading\n- Check video URL is accessible and CORS-enabled\n- Verify video file is not corrupted\n- Check browser console for specific errors\n- Try increasing `retryAttempts` option\n\n### Performance issues\n- Enable `lazyLoadVideo` for multiple viewers on one page\n- Optimize video file size and format\n- Consider using shorter video clips\n\n### Autoplay not working\n- Verify `autoplay: true` is set in options\n- Check that video is muted (required for autoplay in browsers)\n- Desktop: Ensure you're hovering over the badge\n- Mobile: Use long-press instead (autoplay works differently)\n\n## 🔧 Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Development mode with watch\npnpm dev\n\n# Build for production\npnpm build\n\n# Run playground\ncd playground\npnpm dev\n```\n\n## 📄 License\n\nMIT License - see [LICENSE](LICENSE) file for details\n\n## 🤝 Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add some amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n## 💖 Support\n\nIf you find this project helpful, please consider:\n- ⭐ Starring the repository\n- 🐛 Reporting bugs\n- 💡 Suggesting new features\n- 📖 Improving documentation\n\n## 📬 Contact\n\n- Author: Icey Wu\n- Email: 3128006406@qq.com\n- GitHub: [@IceyWu](https://github.com/iceywu)\n\n## 🙏 Acknowledgments\n\nInspired by Apple's Live Photos feature on iOS devices.\n\n---\n\nMade with ❤️ by [Icey Wu](https://github.com/iceywu)\n\n## � Usage Examples\n\n### Vanilla JavaScript\n\n[View complete HTML demo](./demo/html-demo.html)\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003ctitle\u003eLive Photo Demo\u003c/title\u003e\n  \u003cscript src=\"https://fastly.jsdelivr.net/npm/live-photo@latest\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cdiv id=\"live-photo-container\"\u003e\u003c/div\u003e\n\n  \u003cscript\u003e\n    document.addEventListener(\"DOMContentLoaded\", function () {\n      const container = document.getElementById(\"live-photo-container\");\n      \n      const viewer = new LivePhotoViewer({\n        photoSrc: \"https://example.com/photo.jpg\",\n        videoSrc: \"https://example.com/video.mp4\",\n        container: container,\n        width: 400,\n        height: 600,\n        borderRadius: \"12px\",\n        autoplay: true,\n        lazyLoadVideo: true,\n        enableVibration: true,\n        imageCustomization: {\n          styles: {\n            objectFit: \"cover\",\n          },\n          attributes: {\n            alt: \"Beautiful Live Photo\",\n            loading: \"lazy\",\n          },\n        },\n        videoCustomization: {\n          styles: {\n            objectFit: \"cover\",\n          },\n        },\n        // Event callbacks\n        onPhotoLoad: (event, photo) =\u003e {\n          console.log(\"Photo loaded\", photo.naturalWidth, \"x\", photo.naturalHeight);\n        },\n        onVideoLoad: (duration, event, video) =\u003e {\n          console.log(`Video loaded, duration: ${duration}s`);\n        },\n        onProgress: (progress, event, video) =\u003e {\n          console.log(`Loading: ${progress}%`);\n        },\n        onError: (error, event) =\u003e console.error(\"Error:\", error),\n        onClick: (event) =\u003e console.log(\"Clicked!\"),\n      });\n\n      // Control playback programmatically\n      // viewer.play();\n      // viewer.pause();\n      // viewer.stop();\n      // viewer.toggle();\n      \n      // Get current state\n      // const state = viewer.getState();\n      // console.log(state.isPlaying, state.autoplay);\n    });\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Vue 3 (Composition API)\n\n[View complete Vue 3 demo](./demo/vue3-demo.html)\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv ref=\"containerRef\"\u003e\u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript setup lang=\"ts\"\u003e\nimport { ref, onMounted, onUnmounted } from \"vue\";\nimport { LivePhotoViewer } from \"live-photo\";\nimport type { LivePhotoAPI } from \"live-photo\";\n\nconst containerRef = ref\u003cHTMLElement | null\u003e(null);\nconst viewerInstance = ref\u003cLivePhotoAPI | null\u003e(null);\n\nonMounted(() =\u003e {\n  if (containerRef.value) {\n    viewerInstance.value = new LivePhotoViewer({\n      photoSrc: \"https://example.com/photo.jpg\",\n      videoSrc: \"https://example.com/video.mp4\",\n      container: containerRef.value,\n      width: 400,\n      height: 600,\n      borderRadius: \"12px\",\n      autoplay: true,\n      lazyLoadVideo: true,\n      enableVibration: true,\n      imageCustomization: {\n        styles: {\n          objectFit: \"cover\",\n        },\n        attributes: {\n          alt: \"Beautiful Live Photo\",\n          loading: \"lazy\",\n        },\n      },\n      videoCustomization: {\n        styles: {\n          objectFit: \"cover\",\n        },\n      },\n      onPhotoLoad: (event, photo) =\u003e {\n        console.log(\"Photo loaded\", photo.naturalWidth, \"x\", photo.naturalHeight);\n      },\n      onVideoLoad: (duration, event, video) =\u003e {\n        console.log(`Video loaded, duration: ${duration}s`);\n      },\n      onProgress: (progress, event, video) =\u003e {\n        console.log(`Loading: ${progress}%`);\n      },\n      onError: (error, event) =\u003e console.error(\"Error:\", error),\n      onClick: (event) =\u003e console.log(\"Clicked!\"),\n    });\n  }\n});\n\n// Clean up on component unmount\nonUnmounted(() =\u003e {\n  if (viewerInstance.value) {\n    viewerInstance.value.destroy();\n  }\n});\n\n// Example: Control methods\nconst play = () =\u003e viewerInstance.value?.play();\nconst pause = () =\u003e viewerInstance.value?.pause();\nconst toggle = () =\u003e viewerInstance.value?.toggle();\n\u003c/script\u003e\n```\n\n### React (TypeScript)\n\n[View complete React demo](./demo/react-demo.html)\n\n```tsx\nimport React, { useEffect, useRef } from \"react\";\nimport { LivePhotoViewer } from \"live-photo\";\nimport type { LivePhotoAPI } from \"live-photo\";\n\nconst LivePhotoComponent: React.FC = () =\u003e {\n  const containerRef = useRef\u003cHTMLDivElement\u003e(null);\n  const viewerRef = useRef\u003cLivePhotoAPI | null\u003e(null);\n\n  useEffect(() =\u003e {\n    if (containerRef.current) {\n      viewerRef.current = new LivePhotoViewer({\n        photoSrc: \"https://example.com/photo.jpg\",\n        videoSrc: \"https://example.com/video.mp4\",\n        container: containerRef.current,\n        width: 400,\n        height: 600,\n        borderRadius: \"12px\",\n        autoplay: true,\n        lazyLoadVideo: true,\n        enableVibration: true,\n        imageCustomization: {\n          styles: {\n            objectFit: \"cover\",\n          },\n          attributes: {\n            alt: \"Beautiful Live Photo\",\n            loading: \"lazy\",\n          },\n        },\n        videoCustomization: {\n          styles: {\n            objectFit: \"cover\",\n          },\n        },\n        onPhotoLoad: (event, photo) =\u003e {\n          console.log(\"Photo loaded\", photo.naturalWidth, \"x\", photo.naturalHeight);\n        },\n        onVideoLoad: (duration, event, video) =\u003e {\n          console.log(`Video loaded, duration: ${duration}s`);\n        },\n        onProgress: (progress, event, video) =\u003e {\n          console.log(`Loading: ${progress}%`);\n        },\n        onError: (error, event) =\u003e console.error(\"Error:\", error),\n        onClick: (event) =\u003e console.log(\"Clicked!\"),\n      });\n    }\n\n    // Cleanup on unmount\n    return () =\u003e {\n      if (viewerRef.current) {\n        viewerRef.current.destroy();\n      }\n    };\n  }, []);\n\n  // Example: Control methods\n  const handlePlay = () =\u003e viewerRef.current?.play();\n  const handlePause = () =\u003e viewerRef.current?.pause();\n  const handleToggle = () =\u003e viewerRef.current?.toggle();\n\n  return (\n    \u003cdiv\u003e\n      \u003cdiv ref={containerRef}\u003e\u003c/div\u003e\n      \u003cdiv\u003e\n        \u003cbutton onClick={handlePlay}\u003ePlay\u003c/button\u003e\n        \u003cbutton onClick={handlePause}\u003ePause\u003c/button\u003e\n        \u003cbutton onClick={handleToggle}\u003eToggle\u003c/button\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n};\n\nexport default LivePhotoComponent;\n```\n\n### Advanced Usage\n\n#### Accessing Callback Parameters\n\nAll callbacks now provide the original event object and related elements, giving you access to complete DOM information:\n\n```javascript\nconst viewer = new LivePhotoViewer({\n  photoSrc: \"photo.jpg\",\n  videoSrc: \"video.mp4\",\n  container: document.getElementById(\"container\"),\n  \n  onVideoLoad: (duration, event, video) =\u003e {\n    // duration: Video duration in seconds (HTML5 Video API standard unit)\n    console.log(`Video duration: ${duration}s`);\n    \n    // event: Original DOM event object\n    console.log(\"Event type:\", event.type); // \"loadedmetadata\"\n    console.log(\"Is trusted:\", event.isTrusted);\n    \n    // video: HTMLVideoElement element\n    console.log(\"Video dimensions:\", video.videoWidth, \"x\", video.videoHeight);\n    console.log(\"Current time:\", video.currentTime);\n    console.log(\"Ready state:\", video.readyState);\n  },\n  \n  onProgress: (progress, event, video) =\u003e {\n    // progress: Loading progress percentage (0-100)\n    console.log(`Progress: ${progress}%`);\n    \n    // Access more information through the video element\n    if (video.buffered.length \u003e 0) {\n      const bufferedEnd = video.buffered.end(0);\n      console.log(`Buffered: ${bufferedEnd}s`);\n    }\n  },\n  \n  onPhotoLoad: (event, photo) =\u003e {\n    // photo: HTMLImageElement element\n    console.log(\"Image natural size:\", photo.naturalWidth, \"x\", photo.naturalHeight);\n    console.log(\"Image display size:\", photo.width, \"x\", photo.height);\n    console.log(\"Image source:\", photo.src);\n  },\n  \n  onError: (error, event) =\u003e {\n    console.log(\"Error type:\", error.type);\n    console.log(\"Error message:\", error.message);\n    // event is optional, some errors may not have an associated event\n    if (event) {\n      console.log(\"Error event:\", event);\n    }\n  },\n});\n```\n\n#### Lazy Loading with Intersection Observer\n\n```javascript\nconst viewer = new LivePhotoViewer({\n  photoSrc: \"photo.jpg\",\n  videoSrc: \"video.mp4\",\n  container: document.getElementById(\"container\"),\n  lazyLoadVideo: true, // Video loads only when viewer is in viewport\n  onLoadStart: () =\u003e {\n    console.log(\"Video loading started\");\n  },\n  onProgress: (progress, event, video) =\u003e {\n    console.log(`Video buffering: ${progress}%`);\n    console.log(`Buffered: ${video.buffered.length \u003e 0 ? video.buffered.end(0) : 0}s`);\n  },\n});\n```\n\n#### Custom Error Handling\n\n```javascript\nconst viewer = new LivePhotoViewer({\n  photoSrc: \"photo.jpg\",\n  videoSrc: \"video.mp4\",\n  container: document.getElementById(\"container\"),\n  retryAttempts: 5, // Retry 5 times on failure\n  onError: (error, event) =\u003e {\n    console.log(\"Original event:\", event);\n    switch (error.type) {\n      case 'VIDEO_LOAD_ERROR':\n        console.error(\"Failed to load video:\", error.message);\n        // Show fallback UI\n        break;\n      case 'PHOTO_LOAD_ERROR':\n        console.error(\"Failed to load photo:\", error.message);\n        break;\n      case 'PLAYBACK_ERROR':\n        console.error(\"Playback failed:\", error.message);\n        break;\n    }\n  },\n});\n```\n\n#### State Subscription\n\n```javascript\nconst viewer = new LivePhotoViewer({\n  photoSrc: \"photo.jpg\",\n  videoSrc: \"video.mp4\",\n  container: document.getElementById(\"container\"),\n});\n\n// Get current state\nconst state = viewer.getState();\nconsole.log(state.isPlaying); // false\nconsole.log(state.autoplay);  // true\n\n// Note: For reactive state updates, you can poll getState() \n// or use the event callbacks\n```\n\n#### Responsive Sizing\n\n```javascript\nconst viewer = new LivePhotoViewer({\n  photoSrc: \"photo.jpg\",\n  videoSrc: \"video.mp4\",\n  container: document.getElementById(\"container\"),\n  width: \"100%\",      // Responsive width\n  height: \"50vh\",     // 50% of viewport height\n  borderRadius: \"1rem\",\n  imageCustomization: {\n    styles: {\n      objectFit: \"cover\",  // Maintain aspect ratio\n    },\n  },\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficeywu%2Flive-photo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ficeywu%2Flive-photo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficeywu%2Flive-photo/lists"}