test / client /src /Player.jsx
akborana4's picture
Update client/src/Player.jsx
5bdead2 verified
raw
history blame
5.78 kB
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 (
<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, position:'relative' }}>
<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>
{mediaType === 'video' ? (
<video
ref={videoRef}
src={currentSrc}
onError={onMediaError}
controls={isHost}
playsInline
style={{ width:'100%', maxHeight: '56vh', background:'#000' }}
crossOrigin="anonymous"
/>
) : mediaType === 'audio' ? (
<audio
ref={audioRef}
src={currentSrc}
onError={onMediaError}
controls={isHost}
style={{ width:'100%' }}
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>
)}
{err && <div style={{ marginTop:8, color:'var(--bad)' }}>{err}</div>}
</div>
);
}