import React, { useEffect, useRef } from 'react'; import { useToasts } from './Toasts.jsx'; import { log } from './logger.js'; function extractVideoId(u) { try { if (!u) return null; if (/^[a-zA-Z0-9_-]{11}$/.test(u)) return u; const url = new URL(u); if (url.hostname.includes('youtu.be')) return url.pathname.replace('/', ''); if (url.searchParams.get('v')) return url.searchParams.get('v'); const m = url.pathname.match(/\/embed\/([a-zA-Z0-9_-]{11})/); if (m) return m[1]; } catch {} return null; } let apiLoading = false; let apiReady = false; const readyCallbacks = []; function ensureYTApi(cb) { if (apiReady) return cb(); readyCallbacks.push(cb); if (apiLoading) return; apiLoading = true; const tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; window.onYouTubeIframeAPIReady = () => { apiReady = true; readyCallbacks.splice(0).forEach(fn => fn()); }; document.head.appendChild(tag); } export default function YouTubePlayer({ socket, roomId, state, isHost }) { const { push } = useToasts(); const containerRef = useRef(null); const playerRef = useRef(null); const lastSyncRef = useRef(0); useEffect(() => { ensureYTApi(() => { if (!containerRef.current) return; if (playerRef.current && playerRef.current.destroy) { try { playerRef.current.destroy(); } catch {} } const vid = extractVideoId(state?.track?.url); if (!vid) { push('Invalid YouTube URL or ID', 'bad'); return; } // eslint-disable-next-line no-undef playerRef.current = new YT.Player(containerRef.current, { width: '100%', height: '390', videoId: vid, playerVars: { modestbranding: 1, rel: 0, playsinline: 1, controls: isHost ? 1 : 0 }, events: { onReady: (e) => { try { const target = state.isPlaying ? Math.max(0, (state.anchor || 0) + (Date.now() - (state.anchorAt || 0)) / 1000) : (state.anchor || 0); e.target.seekTo(target, true); if (state.isPlaying) e.target.playVideo(); else e.target.pauseVideo(); } catch (err) { log.error('YT onReady error', err); } }, onStateChange: (ev) => { const P = window.YT.PlayerState; if (ev.data === P.ENDED && isHost) socket.emit('ended', { roomId }); }, onError: (e) => { log.error('YouTube error', e); push('YouTube playback error', 'bad'); } } }); }); return () => { if (playerRef.current && playerRef.current.destroy) { try { playerRef.current.destroy(); } catch {} } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [state?.track?.url, isHost]); useEffect(() => { const p = playerRef.current; if (!p || !p.getPlayerState) return; const now = Date.now(); if (now - lastSyncRef.current < 250) return; lastSyncRef.current = now; try { const P = window.YT.PlayerState; const target = state.isPlaying ? Math.max(0, (state.anchor || 0) + (Date.now() - (state.anchorAt || 0)) / 1000) : (state.anchor || 0); const cur = p.getCurrentTime ? p.getCurrentTime() : 0; if (Math.abs(cur - target) > 0.4) p.seekTo(target, true); const isPlayingNow = p.getPlayerState && p.getPlayerState() === P.PLAYING; if (state.isPlaying && !isPlayingNow) p.playVideo(); if (!state.isPlaying && isPlayingNow) p.pauseVideo(); } catch (e) { log.warn('YT sync warn', e); } }, [state.isPlaying, state.anchor, state.anchorAt]); return (