Spaces:
Sleeping
Sleeping
| import React, { useEffect, useMemo, useState } from 'react'; | |
| import ReactPlayer from 'react-player'; | |
| import { getThumb, safeTitle } from './utils.js'; | |
| import { useToasts } from './Toasts.jsx'; | |
| export default function Player({ socket, roomId, state, isHost }) { | |
| const { push } = useToasts(); | |
| const [useProxy, setUseProxy] = useState(false); | |
| const mediaUrl = useMemo(() => { | |
| if (!state?.track?.url) return ''; | |
| return useProxy | |
| ? `/api/proxy?url=${encodeURIComponent(state.track.url)}` | |
| : state.track.url; | |
| }, [state?.track?.url, useProxy]); | |
| const title = state?.track ? safeTitle(state.track) : 'No track selected'; | |
| const thumb = state?.track?.thumb || getThumb(state?.track?.meta); | |
| // Auto-advance when ended (host only) | |
| const handleEnded = () => { | |
| if (isHost) socket.emit('ended', { roomId }); | |
| }; | |
| const handleError = (e) => { | |
| console.warn('ReactPlayer error', e); | |
| if (!useProxy) { | |
| setUseProxy(true); | |
| push('CORS blocked direct URL, retrying via proxy…', 'warn', 3000); | |
| } else { | |
| push('Playback failed (even via proxy)', 'bad', 4000); | |
| } | |
| }; | |
| return ( | |
| <div style={{ position: 'relative' }}> | |
| <div | |
| className="hero-thumb" | |
| style={{ backgroundImage: thumb ? `url("${thumb}")` : undefined }} | |
| aria-hidden | |
| /> | |
| <div className="ambient"></div> | |
| <div className="now-playing" style={{ marginBottom: 10 }}> | |
| <div | |
| className="thumb" | |
| style={{ | |
| width: 72, | |
| height: 72, | |
| backgroundImage: thumb ? `url("${thumb}")` : undefined | |
| }} | |
| /> | |
| <div style={{ minWidth: 0, flex: 1 }}> | |
| <div | |
| style={{ | |
| fontWeight: 700, | |
| overflow: 'hidden', | |
| textOverflow: 'ellipsis', | |
| whiteSpace: 'nowrap' | |
| }} | |
| > | |
| {title} | |
| </div> | |
| <div | |
| style={{ | |
| fontSize: 12, | |
| color: 'var(--muted)', | |
| overflow: 'hidden', | |
| textOverflow: 'ellipsis', | |
| whiteSpace: 'nowrap' | |
| }} | |
| > | |
| {state?.track?.meta?.artists?.join?.(', ') || | |
| state?.track?.meta?.artist || | |
| state?.track?.meta?.album || | |
| state?.track?.kind?.toUpperCase()} | |
| </div> | |
| </div> | |
| {!isHost && <div className="tag">Listener</div>} | |
| </div> | |
| {state?.track?.url ? ( | |
| <ReactPlayer | |
| url={mediaUrl} | |
| playing={state.isPlaying} | |
| controls={isHost} | |
| onEnded={handleEnded} | |
| onError={handleError} | |
| width="100%" | |
| height={state.track.kind === 'video' ? '56vh' : '50px'} | |
| config={{ | |
| file: { | |
| attributes: { | |
| crossOrigin: 'anonymous' | |
| } | |
| } | |
| }} | |
| /> | |
| ) : ( | |
| <div style={{ color: 'var(--muted)', padding: '12px 0' }}> | |
| No track selected | |
| </div> | |
| )} | |
| {useProxy && ( | |
| <div style={{ marginTop: 8, fontSize: 12, color: 'var(--muted)' }}> | |
| Using proxy due to CORS on the original media URL. | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |