Skip to content

LC-1739 Add YouTube Video Support to Lightbox #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ React-spring-lightbox is a flexible image gallery lightbox with native-feeling t
- :mag_right:  Double/Single-tap or double/single-click to zoom in/out
- :ok_hand:    Pinch to zoom
- :point_left:  Panning on zoomed-in images
- :movie_camera:  YouTube video support with event tracking
- :checkered_flag:  Highly performant spring based animations via [react-spring](https://github.com/react-spring/react-spring)
- No external CSS
- Implement your own UI
Expand Down Expand Up @@ -134,6 +135,90 @@ export default CoolLightbox;
| className | Classes are applied to the root lightbox component |
| style | Inline styles are applied to the root lightbox component |
| pageTransitionConfig | React-Spring useTransition config for page open/close animation |
| showVideo | Whether to show video instead of images |
| videoId | YouTube video ID for embedding video content |
| onPlayerEvent | Callback for YouTube player events (play, pause, end, etc.) |

## Video Support

The Lightbox supports YouTube video embeds alongside images. Key features include:

- Support for embedding YouTube videos within the lightbox
- Event handling for video playback and interactions
- Seamless integration with existing image gallery functionality

### VideoEmbed Component

The `VideoEmbed` component is an internal component used by the Lightbox to handle YouTube video playback:

| Prop | Description |
| ------------------ | ------------------------------------------------------------------------------------ |
| videoId | YouTube video ID for embedding video content |
| onPlayerEvent | Callback for YouTube player events (play, pause, end, etc.) |
| className | Optional CSS class name |
| inline | Affects width calculation method, depending on whether the Lightbox is inline or not |
| onNext | Function to navigate to next item |
| onPrev | Function to navigate to previous item |
| onVideoInteraction | Callback when video interaction starts/ends |
| style | Optional inline styles |
| title | Optional title for the iframe (default: "YouTube video player") |

```typescript
import { VideoEmbed } from 'react-spring-lightbox';

// Usage
<VideoEmbed
videoId="youtube-video-id"
className="custom-video-class"
inline={false}
onNext={() => console.log('Next')}
onPrev={() => console.log('Previous')}
onPlayerEvent={(event) => {
console.log('Player event:', event);
// event.type: 'ready' | 'play' | 'pause' | 'end' | 'error' | 'stateChange' | 'playbackRateChange' | 'playbackQualityChange'
// event.data: any
// event.player: YouTube player instance
}}
onVideoInteraction={(isInteracting) => {
console.log('Video interaction:', isInteracting);
}}
style={{ maxWidth: '100%' }}
title="Custom video title"
/>
```

The component uses the YouTube IFrame Player API via `react-youtube` and provides:

- Automatic player initialization and cleanup
- Event tracking for video interactions
- Responsive sizing within the lightbox
- Support for all YouTube player events

The component handles the following events:

- `ready`: When the player is ready to receive commands
- `play`: When the video starts playing
- `pause`: When the video is paused
- `end`: When the video reaches the end
- `error`: When an error occurs in the player
- `stateChange`: When the player's state changes
- `playbackRateChange`: When the playback rate changes
- `playbackQualityChange`: When the playback quality changes

### Lightbox Video Example

Here's an example of how to use the Lightbox with video support:

```javascript
<Lightbox
showVideo={true}
videoId="youtube-video-id"
onPlayerEvent={(event) => {
// Handle video events
}}
// ... other Lightbox props
/>
```

## Local Development

Expand Down
162 changes: 162 additions & 0 deletions example/components/VideoLightbox/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import Lightbox from 'react-spring-lightbox';
import LightboxArrowButton from '../GalleryLightbox/components/LightboxArrowButton';
import LightboxHeader from '../GalleryLightbox/components/LightboxHeader';

const VideoLightbox = ({ galleryTitle, images }) => {
console.log('VideoLightbox mounted');

const [currentImageIndex, setCurrentIndex] = React.useState(0);
const [showVideo, setShowVideo] = React.useState(false);
const [videoId, setVideoId] = React.useState('');
const [isPlaying, setIsPlaying] = React.useState(false);
const [videoDuration, setVideoDuration] = React.useState(0);
const [lastKnownTime, setLastKnownTime] = React.useState(0);
const [player, setPlayer] = React.useState(null);

const canPrev = currentImageIndex > 0;
const canNext = currentImageIndex + 1 < images.length;

React.useEffect(() => {
console.log('Video state changed:', { showVideo, videoId });
if (images[currentImageIndex].type === 'video') {
setShowVideo(true);
setVideoId(images[currentImageIndex].videoId);
} else {
setShowVideo(false);
setVideoId('');
}
}, [currentImageIndex, images]);

const handlePlayerEvent = (event) => {
if (event.type === 'ready') {
setPlayer(event.player);
setVideoDuration(event.player.getDuration());
}

if (event.type === 'play') {
setIsPlaying(true);
console.log('Video started playing');
}

if (event.type === 'pause' || event.type === 'end') {
setIsPlaying(false);
const currentTime = event.player.getCurrentTime();
setLastKnownTime(currentTime);
const completed = currentTime >= videoDuration;
console.log(
'Video ended. Duration watched:',
currentTime,
'seconds',
completed ? '(completed)' : '(incomplete)',
);
}

// Track time updates
if (event.type === 'timeupdate') {
setLastKnownTime(event.player.getCurrentTime());
}
};

const logVideoDuration = () => {
if (showVideo && isPlaying && player) {
const currentTime = lastKnownTime;
const completed = currentTime >= videoDuration;
console.log(
'Video interrupted. Duration watched:',
currentTime,
'seconds',
completed ? '(completed)' : '(incomplete)',
);
}
};

const gotoNext = () => {
if (canNext) {
logVideoDuration();
setTimeout(() => {
setCurrentIndex(currentImageIndex + 1);
}, 100);
}
};

const gotoPrevious = () => {
if (canPrev) {
logVideoDuration();
setTimeout(() => {
setCurrentIndex(currentImageIndex - 1);
}, 100);
}
};

return (
<Container>
<Lightbox
currentIndex={currentImageIndex}
images={images}
isOpen
onNext={gotoNext}
onPlayerEvent={handlePlayerEvent}
onPrev={gotoPrevious}
renderHeader={() => (
<LightboxHeader
currentIndex={currentImageIndex}
galleryTitle={galleryTitle}
images={images}
onClose={() => {}}
/>
)}
renderNextButton={({ canNext }) => (
<StyledLightboxArrowButton
disabled={!canNext}
onClick={gotoNext}
position="right"
/>
)}
renderPrevButton={({ canPrev }) => (
<StyledLightboxArrowButton
disabled={!canPrev}
onClick={gotoPrevious}
position="left"
/>
)}
showVideo={showVideo}
singleClickToZoom
videoId={videoId}
/>
</Container>
);
};

export default VideoLightbox;

VideoLightbox.propTypes = {
galleryTitle: PropTypes.string,
images: PropTypes.arrayOf(
PropTypes.shape({
alt: PropTypes.string.isRequired,
caption: PropTypes.string,
src: PropTypes.string.isRequired,
type: PropTypes.oneOf(['image', 'video']),
videoId: PropTypes.string,
}),
).isRequired,
};

const Container = styled.div`
display: inline-flex;
flex-direction: column;
width: 100%;
height: 384px;
overflow: hidden;
position: relative;
`;

const StyledLightboxArrowButton = styled(LightboxArrowButton)`
z-index: 10;
button {
font-size: 25px;
}
`;
20 changes: 20 additions & 0 deletions example/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import styled from 'styled-components';
import GalleryLightbox from '../components/GalleryLightbox';
import InlineLightbox from '../components/InlineLightbox';
import Link from 'next/link';

const images = [
{
Expand Down Expand Up @@ -181,6 +182,11 @@ const HomePage = () => {
Switch Image Array
</Button>
</InlineLightboxExampleContainer>
<hr />
<StyledH2>Video in Lightbox</StyledH2>
<Nav>
<Link href="/video-lightbox">Video in Lightbox Example</Link>
</Nav>
</Container>
);
};
Expand Down Expand Up @@ -240,3 +246,17 @@ const OtherInlineContent = styled.div`
const StyledH2 = styled.h2`
text-align: center;
`;

const Nav = styled.nav`
display: flex;
justify-content: center;
margin-bottom: 2em;

a {
color: #7fffd4;
font-weight: bold;
&:hover {
text-decoration: none;
}
}
`;
42 changes: 42 additions & 0 deletions example/pages/video-lightbox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react';
import VideoLightbox from '../components/VideoLightbox';
import styled from 'styled-components';

const images = [
{
alt: 'Sample Video 1',
caption: 'First Video',
src: 'https://picsum.photos/800/600?random=2',
type: 'video',
videoId: 'dQw4w9WgXcQ', // Rick Astley - Never Gonna Give You Up
},
{
alt: 'Sample Image 1',
caption: 'First Image',
src: 'https://picsum.photos/800/600?random=1',
},
{
alt: 'Sample Image 2',
caption: 'Second Image',
src: 'https://picsum.photos/800/600?random=3',
},
];

const VideoLightboxPage = () => {
return (
<PageContainer>
<VideoLightbox
galleryTitle="Video Lightbox Example"
images={images}
/>
</PageContainer>
);
};

export default VideoLightboxPage;

const PageContainer = styled.div`
padding: 32px;
max-width: 1200px;
margin: 0 auto;
`;
Loading