diff --git a/src/components/VideoPlayerWithChapters.tsx b/src/components/VideoPlayerWithChapters.tsx index 07da05c..2557fab 100644 --- a/src/components/VideoPlayerWithChapters.tsx +++ b/src/components/VideoPlayerWithChapters.tsx @@ -30,9 +30,9 @@ export const VideoPlayerWithChapters = forwardRef { const iframeRef = useRef(null); - const intervalRef = useRef(null); const [currentChapterIndex, setCurrentChapterIndex] = useState(-1); const [currentTime, setCurrentTime] = useState(0); + const [isApiReady, setIsApiReady] = useState(false); // Detect if this is a YouTube URL const isYouTube = videoUrl && ( @@ -56,59 +56,71 @@ export const VideoPlayerWithChapters = forwardRef { - if (!accentColor || !isYouTube) return; + if (!isYouTube) return; - const style = document.createElement('style'); - style.textContent = ` - .youtube-wrapper { - position: relative; - padding-bottom: 56.25%; - height: 0; - overflow: hidden; - border-radius: 0.5rem; - } - .youtube-wrapper iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; - } - `; - document.head.appendChild(style); + // Check if API is already loaded + if ((window as any).YT && (window as any).YT.Player) { + setIsApiReady(true); + return; + } + + // Load YouTube IFrame API + const tag = document.createElement('script'); + tag.src = 'https://www.youtube.com/iframe_api'; + const firstScriptTag = document.getElementsByTagName('script')[0]; + if (firstScriptTag && firstScriptTag.parentNode) { + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + } else { + document.body.appendChild(tag); + } + + // Set up callback + (window as any).onYouTubeIframeAPIReady = () => { + setIsApiReady(true); + }; return () => { - document.head.removeChild(style); + (window as any).onYouTubeIframeAPIReady = null; }; - }, [accentColor, isYouTube]); + }, [isYouTube]); - // Time tracking using postMessage (YouTube IFrame API without loading the script) + // Initialize YouTube player and set up tracking useEffect(() => { - if (!isYouTube || !iframeRef.current || chapters.length === 0) return; + if (!isYouTube || !isApiReady || !iframeRef.current) return; - // Listen for time updates from YouTube iframe - const handleMessage = (event: MessageEvent) => { - // Verify the message is from YouTube - if (!event.origin.includes('youtube.com') && !event.origin.includes('googlevideo.com')) { - return; - } + // Wait for iframe to be ready + const initializePlayer = () => { + if (!iframeRef.current || !(window as any).YT) return; - const data = event.data; - if (typeof data === 'string') { + const youtubeId = getYouTubeId(effectiveVideoUrl); + if (!youtubeId) return; + + // Create YouTube player instance + const player = new (window as any).YT.Player(iframeRef.current, { + events: { + onReady: () => { + console.log('YouTube player ready'); + }, + onStateChange: (event: any) => { + // Player state changed + }, + }, + }); + + // Set up time tracking interval + const interval = setInterval(() => { try { - const parsed = JSON.parse(data); - if (parsed.event === 'infoDelivery' && parsed.info && parsed.info.currentTime) { - const time = parsed.info.currentTime; - setCurrentTime(time); + const time = player.getCurrentTime(); + setCurrentTime(time); - if (onTimeUpdate) { - onTimeUpdate(time); - } + if (onTimeUpdate) { + onTimeUpdate(time); + } - // Find current chapter + // Find current chapter + if (chapters.length > 0) { let index = chapters.findIndex((chapter, i) => { const nextChapter = chapters[i + 1]; return time >= chapter.time && (!nextChapter || time < nextChapter.time); @@ -127,32 +139,51 @@ export const VideoPlayerWithChapters = forwardRef { + clearInterval(interval); + if (player && player.destroy) { + player.destroy(); + } + }; }; - window.addEventListener('message', handleMessage); + const timeout = setTimeout(initializePlayer, 100); return () => { - window.removeEventListener('message', handleMessage); + clearTimeout(timeout); }; - }, [isYouTube, chapters, currentChapterIndex, onChapterChange, onTimeUpdate]); + }, [isYouTube, isApiReady, effectiveVideoUrl, chapters, currentChapterIndex, onChapterChange, onTimeUpdate]); - // Jump to specific time using postMessage + // Jump to specific time using YouTube API const jumpToTime = (time: number) => { - if (iframeRef.current && iframeRef.current.contentWindow) { - // Send seek command to YouTube iframe - iframeRef.current.contentWindow.postMessage( - `{"event":"command","func":"seekTo","args":[${time}, true]}`, - 'https://www.youtube.com' - ); - // Auto-play after seeking - iframeRef.current.contentWindow.postMessage( + const iframe = iframeRef.current; + if (!iframe || !iframe.contentWindow) return; + + // Try YouTube API first + if (isApiReady && (window as any).YT && (window as any).YT.getPlayerById) { + const player = (window as any).YT.getPlayerById(iframe.id); + if (player) { + player.seekTo(time, true); + player.playVideo(); + return; + } + } + + // Fallback: use postMessage + iframe.contentWindow.postMessage( + `{"event":"command","func":"seekTo","args":[${time}, true]}`, + 'https://www.youtube.com' + ); + setTimeout(() => { + iframe.contentWindow.postMessage( `{"event":"command","func":"playVideo","args":[]}`, 'https://www.youtube.com' ); - } + }, 100); }; const getCurrentTime = () => { @@ -178,15 +209,34 @@ export const VideoPlayerWithChapters = forwardRef +
+ {youtubeId && ( -