akborana4 commited on
Commit
ce63e50
·
verified ·
1 Parent(s): 83623a0

Update client/src/Player.jsx

Browse files
Files changed (1) hide show
  1. client/src/Player.jsx +70 -128
client/src/Player.jsx CHANGED
@@ -1,123 +1,39 @@
1
- // client/src/Player.jsx
2
- import React, { useEffect, useMemo, useRef, useState } from 'react';
3
- import { detectMediaTypeFromUrl, getThumb, safeTitle } from './utils.js';
4
  import { useToasts } from './Toasts.jsx';
5
- import { log } from './logger.js';
6
 
7
  export default function Player({ socket, roomId, state, isHost }) {
8
  const { push } = useToasts();
9
- const audioRef = useRef(null);
10
- const videoRef = useRef(null);
11
-
12
- const [err, setErr] = useState(null);
13
  const [useProxy, setUseProxy] = useState(false);
14
- const busyRef = useRef(false);
15
- const lastAppliedRef = useRef({ url: null });
16
-
17
- const mediaType = useMemo(() => {
18
- if (!state?.track?.url) return 'none';
19
- if (state?.track?.kind) return state.track.kind;
20
- return detectMediaTypeFromUrl(state.track.url) || 'audio';
21
- }, [state?.track?.url, state?.track?.kind]);
22
-
23
- const logicalTime = () => {
24
- if (!state) return 0;
25
- const { anchor = 0, anchorAt = 0, isPlaying = false } = state;
26
- if (!isPlaying) return anchor;
27
- const elapsed = (Date.now() - anchorAt) / 1000;
28
- return Math.max(0, anchor + elapsed);
29
- };
30
 
31
- const currentSrc = useMemo(() => {
32
  if (!state?.track?.url) return '';
33
- return useProxy ? `/api/proxy?url=${encodeURIComponent(state.track.url)}` : state.track.url;
 
 
34
  }, [state?.track?.url, useProxy]);
35
 
36
- const applyMediaState = async (el) => {
37
- if (!el || !state?.track?.url) return;
38
- if (busyRef.current) return;
39
- busyRef.current = true;
40
- try {
41
- const marker = `${state.track.url}|${useProxy ? 'px' : 'dir'}`;
42
- if (lastAppliedRef.current.url !== marker) {
43
- el.src = currentSrc;
44
- el.load();
45
- lastAppliedRef.current.url = marker;
46
- }
47
- const target = logicalTime();
48
- if (Number.isFinite(target)) {
49
- try { el.currentTime = Math.max(0, target); } catch {}
50
- }
51
- if (state.isPlaying) {
52
- await el.play().catch(() => {});
53
- } else {
54
- el.pause();
55
- }
56
- setErr(null);
57
- } catch (e) {
58
- log.error('Playback error', e);
59
- setErr(e?.message || 'Playback failed');
60
- push(`Playback failed: ${e?.message || 'Unknown error'}`, 'bad', 4500);
61
- } finally {
62
- busyRef.current = false;
63
- }
64
- };
65
-
66
- // When track or type changes, reset proxy and apply
67
- useEffect(() => {
68
- setUseProxy(false);
69
- lastAppliedRef.current.url = null;
70
- const el = mediaType === 'video' ? videoRef.current : audioRef.current;
71
- if (el) applyMediaState(el);
72
- // eslint-disable-next-line react-hooks/exhaustive-deps
73
- }, [state?.track?.url, mediaType]);
74
-
75
- // Sync drift and play/pause
76
- useEffect(() => {
77
- const el = mediaType === 'video' ? videoRef.current : audioRef.current;
78
- if (!el) return;
79
- const target = logicalTime();
80
- const delta = target - el.currentTime;
81
- if (Math.abs(delta) > 0.35) {
82
- try { el.currentTime = Math.max(0, target); } catch {}
83
- }
84
- if (state.isPlaying && el.paused) {
85
- el.play().catch(() => {});
86
- } else if (!state.isPlaying && !el.paused) {
87
- el.pause();
88
- }
89
- // eslint-disable-next-line react-hooks/exhaustive-deps
90
- }, [state.isPlaying, state.anchor, state.anchorAt]);
91
 
92
- // Auto-advance
93
- useEffect(() => {
94
- const onEnded = () => { if (isHost) socket.emit('ended', { roomId }); };
95
- const a = audioRef.current, v = videoRef.current;
96
- a?.addEventListener('ended', onEnded);
97
- v?.addEventListener('ended', onEnded);
98
- return () => {
99
- a?.removeEventListener('ended', onEnded);
100
- v?.removeEventListener('ended', onEnded);
101
- };
102
- }, [socket, roomId, isHost]);
103
 
104
- // On media error, retry via proxy once
105
- const onMediaError = () => {
106
  if (!useProxy) {
107
  setUseProxy(true);
108
- lastAppliedRef.current.url = null;
109
- const el = mediaType === 'video' ? videoRef.current : audioRef.current;
110
- if (el) applyMediaState(el);
111
  } else {
112
- setErr('Playback failed (even via proxy)');
113
  }
114
  };
115
 
116
- const title = state?.track ? safeTitle(state.track) : 'No track selected';
117
- const thumb = state?.track?.thumb || getThumb(state?.track?.meta);
118
-
119
  return (
120
- <div style={{ position:'relative' }}>
121
  <div
122
  className="hero-thumb"
123
  style={{ backgroundImage: thumb ? `url("${thumb}")` : undefined }}
@@ -125,46 +41,72 @@ export default function Player({ socket, roomId, state, isHost }) {
125
  />
126
  <div className="ambient"></div>
127
 
128
- <div className="now-playing" style={{ marginBottom: 10, position:'relative' }}>
129
- <div className="thumb" style={{ width:72, height:72, backgroundImage: thumb ? `url("${thumb}")` : undefined }} />
130
- <div style={{ minWidth:0, flex:1 }}>
131
- <div style={{ fontWeight:700, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{title}</div>
132
- <div style={{ fontSize:12, color:'var(--muted)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
133
- {state?.track?.meta?.artists?.join?.(', ') || state?.track?.meta?.artist || state?.track?.meta?.album || state?.track?.kind?.toUpperCase()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </div>
135
  </div>
136
  {!isHost && <div className="tag">Listener</div>}
137
  </div>
138
 
139
- {mediaType === 'video' ? (
140
- <video
141
- ref={videoRef}
142
- src={currentSrc}
143
- onError={onMediaError}
144
  controls={isHost}
145
- playsInline
146
- style={{ width:'100%', maxHeight: '56vh', background:'#000' }}
147
- crossOrigin="anonymous"
148
- />
149
- ) : mediaType === 'audio' ? (
150
- <audio
151
- ref={audioRef}
152
- src={currentSrc}
153
- onError={onMediaError}
154
- controls={isHost}
155
- style={{ width:'100%' }}
156
- crossOrigin="anonymous"
157
  />
158
  ) : (
159
- <div style={{ color:'var(--muted)', padding:'12px 0' }}>No track selected</div>
 
 
160
  )}
161
 
162
  {useProxy && (
163
- <div style={{ marginTop:8, fontSize:12, color:'var(--muted)' }}>
164
  Using proxy due to CORS on the original media URL.
165
  </div>
166
  )}
167
- {err && <div style={{ marginTop:8, color:'var(--bad)' }}>{err}</div>}
168
  </div>
169
  );
170
  }
 
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import ReactPlayer from 'react-player';
3
+ import { getThumb, safeTitle } from './utils.js';
4
  import { useToasts } from './Toasts.jsx';
 
5
 
6
  export default function Player({ socket, roomId, state, isHost }) {
7
  const { push } = useToasts();
 
 
 
 
8
  const [useProxy, setUseProxy] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ const mediaUrl = useMemo(() => {
11
  if (!state?.track?.url) return '';
12
+ return useProxy
13
+ ? `/api/proxy?url=${encodeURIComponent(state.track.url)}`
14
+ : state.track.url;
15
  }, [state?.track?.url, useProxy]);
16
 
17
+ const title = state?.track ? safeTitle(state.track) : 'No track selected';
18
+ const thumb = state?.track?.thumb || getThumb(state?.track?.meta);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ // Auto-advance when ended (host only)
21
+ const handleEnded = () => {
22
+ if (isHost) socket.emit('ended', { roomId });
23
+ };
 
 
 
 
 
 
 
24
 
25
+ const handleError = (e) => {
26
+ console.warn('ReactPlayer error', e);
27
  if (!useProxy) {
28
  setUseProxy(true);
29
+ push('CORS blocked direct URL, retrying via proxy…', 'warn', 3000);
 
 
30
  } else {
31
+ push('Playback failed (even via proxy)', 'bad', 4000);
32
  }
33
  };
34
 
 
 
 
35
  return (
36
+ <div style={{ position: 'relative' }}>
37
  <div
38
  className="hero-thumb"
39
  style={{ backgroundImage: thumb ? `url("${thumb}")` : undefined }}
 
41
  />
42
  <div className="ambient"></div>
43
 
44
+ <div className="now-playing" style={{ marginBottom: 10 }}>
45
+ <div
46
+ className="thumb"
47
+ style={{
48
+ width: 72,
49
+ height: 72,
50
+ backgroundImage: thumb ? `url("${thumb}")` : undefined
51
+ }}
52
+ />
53
+ <div style={{ minWidth: 0, flex: 1 }}>
54
+ <div
55
+ style={{
56
+ fontWeight: 700,
57
+ overflow: 'hidden',
58
+ textOverflow: 'ellipsis',
59
+ whiteSpace: 'nowrap'
60
+ }}
61
+ >
62
+ {title}
63
+ </div>
64
+ <div
65
+ style={{
66
+ fontSize: 12,
67
+ color: 'var(--muted)',
68
+ overflow: 'hidden',
69
+ textOverflow: 'ellipsis',
70
+ whiteSpace: 'nowrap'
71
+ }}
72
+ >
73
+ {state?.track?.meta?.artists?.join?.(', ') ||
74
+ state?.track?.meta?.artist ||
75
+ state?.track?.meta?.album ||
76
+ state?.track?.kind?.toUpperCase()}
77
  </div>
78
  </div>
79
  {!isHost && <div className="tag">Listener</div>}
80
  </div>
81
 
82
+ {state?.track?.url ? (
83
+ <ReactPlayer
84
+ url={mediaUrl}
85
+ playing={state.isPlaying}
 
86
  controls={isHost}
87
+ onEnded={handleEnded}
88
+ onError={handleError}
89
+ width="100%"
90
+ height={state.track.kind === 'video' ? '56vh' : '50px'}
91
+ config={{
92
+ file: {
93
+ attributes: {
94
+ crossOrigin: 'anonymous'
95
+ }
96
+ }
97
+ }}
 
98
  />
99
  ) : (
100
+ <div style={{ color: 'var(--muted)', padding: '12px 0' }}>
101
+ No track selected
102
+ </div>
103
  )}
104
 
105
  {useProxy && (
106
+ <div style={{ marginTop: 8, fontSize: 12, color: 'var(--muted)' }}>
107
  Using proxy due to CORS on the original media URL.
108
  </div>
109
  )}
 
110
  </div>
111
  );
112
  }