diff --git a/src/components/VideoPlayerWithChapters.tsx b/src/components/VideoPlayerWithChapters.tsx index d502b67..0a85ccc 100644 --- a/src/components/VideoPlayerWithChapters.tsx +++ b/src/components/VideoPlayerWithChapters.tsx @@ -2,6 +2,24 @@ import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 're import Plyr from 'plyr'; import 'plyr/dist/plyr.css'; +// YouTube Player API types +declare global { + interface Window { + YT?: { + Player: any; + PlayerState: { + UNSTARTED: number; + ENDED: number; + PLAYING: number; + PAUSED: number; + BUFFERING: number; + VIDEO_CUED: number; + }; + }; + onYouTubeIframeAPIReady?: () => void; + } +} + interface VideoChapter { time: number; // Time in seconds title: string; @@ -35,8 +53,12 @@ export const VideoPlayerWithChapters = forwardRef(null); const playerRef = useRef(null); const posterRef = useRef(null); + const ytPlayerRef = useRef(null); + const intervalRef = useRef(null); const [currentChapterIndex, setCurrentChapterIndex] = useState(-1); const [isPlaying, setIsPlaying] = useState(false); + const [ytPlayerState, setYtPlayerState] = useState(-1); // YouTube player state + const [ytApiReady, setYtApiReady] = useState(false); // Detect if this is a YouTube URL const isYouTube = videoUrl && ( @@ -60,8 +82,27 @@ export const VideoPlayerWithChapters = forwardRef { - if (!wrapperRef.current) return; + if (window.YT && window.YT.Player) { + setYtApiReady(true); + return; + } + + const tag = document.createElement('script'); + tag.src = 'https://www.youtube.com/iframe_api'; + document.body.appendChild(tag); + + window.onYouTubeIframeAPIReady = () => setYtApiReady(true); + + return () => { + window.onYouTubeIframeAPIReady = undefined; + }; + }, []); + + // Initialize Plyr with YouTube API integration + useEffect(() => { + if (!wrapperRef.current || !isYouTube) return; // Initialize Plyr const player = new Plyr(wrapperRef.current, { @@ -94,72 +135,67 @@ export const VideoPlayerWithChapters = forwardRef { + if (!ytApiReady || !wrapperRef.current) return; - // Track play/pause state to show/hide poster overlay with delay - let hidePosterTimeout: NodeJS.Timeout | null = null; + const iframe = wrapperRef.current.querySelector('iframe'); + if (!iframe) return; - player.on('play', () => { - setIsPlaying(true); - // Clear any existing timeout - if (hidePosterTimeout) { - clearTimeout(hidePosterTimeout); - } + // Create YouTube Player instance + const ytPlayer = new window.YT.Player(iframe, { + events: { + onStateChange: (event: any) => { + const state = event.data; + setYtPlayerState(state); - // Wait 500ms before fading out the poster (like your reference) - hidePosterTimeout = setTimeout(() => { - if (posterRef.current) { - posterRef.current.style.opacity = '0'; - posterRef.current.style.pointerEvents = 'none'; + // -1 = unstarted, 0 = ended, 1 = playing, 2 = paused, 3 = buffering, 5 = video cued + if (state === 1) { + setIsPlaying(true); + } else if (state === 2 || state === 0) { + setIsPlaying(false); + } + }, + }, + }); + + ytPlayerRef.current = ytPlayer; + + // Set up time tracking for chapters + intervalRef.current = setInterval(() => { + if (ytPlayer && ytPlayer.getCurrentTime) { + const currentTime = ytPlayer.getCurrentTime(); + if (onTimeUpdate) { + onTimeUpdate(currentTime); + } + + // Find current chapter + if (chapters.length > 0) { + let index = chapters.findIndex((chapter, i) => { + const nextChapter = chapters[i + 1]; + return currentTime >= chapter.time && (!nextChapter || currentTime < nextChapter.time); + }); + + // If before first chapter, no active chapter + if (index === -1 && currentTime < chapters[0].time) { + index = -1; + } + + if (index !== currentChapterIndex) { + setCurrentChapterIndex(index); + if (index >= 0 && onChapterChange) { + onChapterChange(chapters[index]); + } + } + } } }, 500); - }); + }; - player.on('pause', () => { - setIsPlaying(false); - // Clear timeout if paused before poster hides - if (hidePosterTimeout) { - clearTimeout(hidePosterTimeout); - hidePosterTimeout = null; - } - if (posterRef.current) { - posterRef.current.style.opacity = '1'; - posterRef.current.style.pointerEvents = 'auto'; - } - }); - - player.on('ended', () => { - setIsPlaying(false); - // Clear timeout - if (hidePosterTimeout) { - clearTimeout(hidePosterTimeout); - hidePosterTimeout = null; - } - if (posterRef.current) { - posterRef.current.style.opacity = '1'; - posterRef.current.style.pointerEvents = 'auto'; - } - }); + // Initialize YouTube player when API is ready + if (ytApiReady) { + initializeYouTubePlayer(); + } // Apply custom accent color if (accentColor) { @@ -186,59 +222,37 @@ export const VideoPlayerWithChapters = forwardRef { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + if (ytPlayerRef.current && ytPlayerRef.current.destroy) { + ytPlayerRef.current.destroy(); + } player.destroy(); }; - }, [accentColor]); + }, [isYouTube, ytApiReady, accentColor, chapters, currentChapterIndex, onChapterChange, onTimeUpdate]); - // Handle chapter tracking - useEffect(() => { - if (!playerRef.current || chapters.length === 0) return; - - const player = playerRef.current; - - const updateTime = () => { - const currentTime = player.currentTime; - - // Report time update to parent - if (onTimeUpdate) { - onTimeUpdate(currentTime); - } - - // Find current chapter - let index = chapters.findIndex((chapter, i) => { - const nextChapter = chapters[i + 1]; - return currentTime >= chapter.time && (!nextChapter || currentTime < nextChapter.time); - }); - - // If before first chapter, no active chapter - if (index === -1 && currentTime < chapters[0].time) { - index = -1; - } - - if (index !== currentChapterIndex) { - setCurrentChapterIndex(index); - if (index >= 0 && onChapterChange) { - onChapterChange(chapters[index]); - } - } - }; - - player.on('timeupdate', updateTime); - - return () => { - player.off('timeupdate', updateTime); - }; - }, [chapters, currentChapterIndex, onChapterChange, onTimeUpdate]); + // Prevent context menu and unwanted interactions + const preventAction = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; // Jump to specific time const jumpToTime = (time: number) => { - if (playerRef.current && isYouTube) { + if (ytPlayerRef.current) { + ytPlayerRef.current.seekTo(time, true); + ytPlayerRef.current.playVideo(); + } else if (playerRef.current) { playerRef.current.currentTime = time; playerRef.current.play(); } }; const getCurrentTime = () => { + if (ytPlayerRef.current) { + return ytPlayerRef.current.getCurrentTime() || 0; + } return playerRef.current ? playerRef.current.currentTime : 0; }; @@ -263,12 +277,108 @@ export const VideoPlayerWithChapters = forwardRef {youtubeId && ( -