import React, { useEffect, useMemo, useRef, useState } from 'react'; import { detectMediaTypeFromUrl, getThumb, safeTitle } from './utils.js'; import { useToasts } from './Toasts.jsx'; import { log } from './logger.js'; export default function Player({ socket, roomId, state, isHost }) { const { push } = useToasts(); const audioRef = useRef(null); const videoRef = useRef(null); const [err, setErr] = useState(null); const [useProxy, setUseProxy] = useState(false); const busyRef = useRef(false); const lastAppliedRef = useRef({ url: null }); const mediaType = useMemo(() => { if (!state?.track?.url) return 'none'; if (state?.track?.kind) return state.track.kind; return detectMediaTypeFromUrl(state.track.url) || 'audio'; }, [state?.track?.url, state?.track?.kind]); const logicalTime = () => { if (!state) return 0; const { anchor = 0, anchorAt = 0, isPlaying = false } = state; if (!isPlaying) return anchor; const elapsed = (Date.now() - anchorAt) / 1000; return Math.max(0, anchor + elapsed); }; const currentSrc = useMemo(() => { if (!state?.track?.url) return ''; return useProxy ? `/api/proxy?url=${encodeURIComponent(state.track.url)}` : state.track.url; }, [state?.track?.url, useProxy]); const applyMediaState = async (el) => { if (!el || !state?.track?.url) return; if (busyRef.current) return; busyRef.current = true; try { const marker = `${state.track.url}|${useProxy ? 'px' : 'dir'}`; if (lastAppliedRef.current.url !== marker) { el.src = currentSrc; el.load(); lastAppliedRef.current.url = marker; } const target = logicalTime(); if (Number.isFinite(target)) { try { el.currentTime = Math.max(0, target); } catch {} } if (state.isPlaying) { await el.play().catch(() => {}); } else { el.pause(); } setErr(null); } catch (e) { log.error('Playback error', e); setErr(e?.message || 'Playback failed'); push(`Playback failed: ${e?.message || 'Unknown error'}`, 'bad', 4500); } finally { busyRef.current = false; } }; useEffect(() => { setUseProxy(false); lastAppliedRef.current.url = null; const el = mediaType === 'video' ? videoRef.current : audioRef.current; if (el) applyMediaState(el); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state?.track?.url, mediaType]); useEffect(() => { const el = mediaType === 'video' ? videoRef.current : audioRef.current; if (!el) return; const target = logicalTime(); const delta = target - el.currentTime; if (Math.abs(delta) > 0.35) { try { el.currentTime = Math.max(0, target); } catch {} } if (state.isPlaying && el.paused) { el.play().catch(() => {}); } else if (!state.isPlaying && !el.paused) { el.pause(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.isPlaying, state.anchor, state.anchorAt]); useEffect(() => { const onEnded = () => { if (isHost) socket.emit('ended', { roomId }); }; const a = audioRef.current, v = videoRef.current; a?.addEventListener('ended', onEnded); v?.addEventListener('ended', onEnded); return () => { a?.removeEventListener('ended', onEnded); v?.removeEventListener('ended', onEnded); }; }, [socket, roomId, isHost]); const onMediaError = () => { if (!useProxy) { setUseProxy(true); lastAppliedRef.current.url = null; const el = mediaType === 'video' ? videoRef.current : audioRef.current; if (el) applyMediaState(el); } else { setErr('Playback failed (even via proxy)'); } }; const title = state?.track ? safeTitle(state.track) : 'No track selected'; const thumb = state?.track?.thumb || getThumb(state?.track?.meta); return (