@@ -16,7 +16,6 @@ import useVolumePreference, { clampVolume } from '@/hooks/useVolumePreference'
1616import { useAppSnackbar } from '@/hooks/useAppSnackbar'
1717import { useWebSocket } from '@/context/WebSocketContext'
1818import { useSpotifyCommand } from '@/hooks/useSpotifyCommand'
19- import useSpotifyWebPlayback from '@/hooks/useSpotifyWebPlayback'
2019import { SpotifyCommand } from '@/types/websocket'
2120import {
2221 HRM_WEB_PLAYER_NAME ,
@@ -31,19 +30,16 @@ const SpotifyControls = () => {
3130 const { spotifyData, connectionStatus, sendData, spotifyServiceInitialized } =
3231 useWebSocket ( )
3332 const { execute : executeSpotify } = useSpotifyCommand ( )
34- const { player, isReady } = useSpotifyWebPlayback ( )
3533 const { devices = [ ] } = spotifyData // Default to empty array if undefined
3634 const { volume, setVolume, muted, toggleMute } = useVolumePreference ( )
3735 const { showWarning } = useAppSnackbar ( )
3836 const lastSentVolumeRef = useRef < string | null > ( null )
3937 const lastWarningTimeRef = useRef < number > ( 0 )
4038 const [ selectedDeviceId , setSelectedDeviceId ] = useState < string > ( '' )
41- const [ isSyncingVolume , setIsSyncingVolume ] = useState ( false )
39+ const [ isSliding , setIsSliding ] = useState ( false )
4240 const prevActiveIdRef = useRef < string | undefined > ( undefined )
4341 const lastVolumeSyncTimeRef = useRef < number > ( 0 )
44- const volumeLockTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > (
45- null
46- )
42+ const hasPendingSendRef = useRef < boolean > ( false )
4743
4844 const hrmDevice = useMemo (
4945 ( ) =>
@@ -105,16 +101,33 @@ const SpotifyControls = () => {
105101 // We rely on the server as the source of truth for volume, but use a grace period
106102 // to prevent local sliders from "jumping" while the user is actively adjusting them.
107103 const playbackVolume = spotifyData . playback . volume_percent
104+
105+ if ( isSliding ) return
106+
107+ const timeSinceLastVolumeSend = Date . now ( ) - lastVolumeSyncTimeRef . current
108+
109+ // Only sync if we haven't sent a volume command recently.
110+ // The server broadcasts a SPOTIFY_UPDATE immediately after a SET_VOLUME command,
111+ // confirming the new state to all clients.
112+ const shouldRespectGracePeriod =
113+ hasPendingSendRef . current &&
114+ timeSinceLastVolumeSend < VOLUME_SYNC_GRACE_PERIOD_MS
115+
116+ if ( shouldRespectGracePeriod ) {
117+ return
118+ }
119+
120+ // Clear pending flag after grace period
121+ if (
122+ hasPendingSendRef . current &&
123+ timeSinceLastVolumeSend >= VOLUME_SYNC_GRACE_PERIOD_MS
124+ ) {
125+ hasPendingSendRef . current = false
126+ }
127+
108128 if ( activeDevice && typeof playbackVolume === 'number' ) {
109- const timeSinceLastVolumeSend = Date . now ( ) - lastVolumeSyncTimeRef . current
110-
111- // Only sync if we haven't sent a volume command recently.
112- // The server broadcasts a SPOTIFY_UPDATE immediately after a SET_VOLUME command,
113- // confirming the new state to all clients.
114- if ( timeSinceLastVolumeSend > VOLUME_SYNC_GRACE_PERIOD_MS ) {
115- if ( playbackVolume !== volume ) {
116- setVolume ( playbackVolume )
117- }
129+ if ( playbackVolume !== volume ) {
130+ setVolume ( playbackVolume )
118131 }
119132 }
120133
@@ -190,6 +203,7 @@ const SpotifyControls = () => {
190203
191204 const handleVolumeChange = useCallback (
192205 ( val : number ) => {
206+ setIsSliding ( true )
193207 setVolume ( val )
194208 if ( connectionStatus !== 'Connected' ) {
195209 const now = Date . now ( )
@@ -205,7 +219,7 @@ const SpotifyControls = () => {
205219
206220 const sendVolumeCommand = useCallback (
207221 ( value : number ) => {
208- if ( connectionStatus !== 'Connected' || isSyncingVolume ) return
222+ if ( connectionStatus !== 'Connected' ) return
209223 const targetDeviceId = resolveTargetDeviceId ( )
210224
211225 // Prevent sending volume command if no device is targeted
@@ -215,58 +229,33 @@ const SpotifyControls = () => {
215229 const messageKey = `${ targetDeviceId } :${ sanitized } `
216230 if ( lastSentVolumeRef . current === messageKey ) return
217231
218- setIsSyncingVolume ( true )
232+ hasPendingSendRef . current = true
233+ lastVolumeSyncTimeRef . current = Date . now ( )
234+
219235 executeSpotify ( 'SET_VOLUME' , {
220236 volume : sanitized ,
221237 deviceId : targetDeviceId ,
222238 } )
223239
224240 lastSentVolumeRef . current = messageKey
225- lastVolumeSyncTimeRef . current = Date . now ( )
226-
227- // Release lock after a short delay to allow state to settle
228- if ( volumeLockTimeoutRef . current ) {
229- clearTimeout ( volumeLockTimeoutRef . current )
230- }
231- volumeLockTimeoutRef . current = setTimeout ( ( ) => {
232- setIsSyncingVolume ( false )
233- volumeLockTimeoutRef . current = null
234- } , 500 )
235241 } ,
236- [ connectionStatus , resolveTargetDeviceId , executeSpotify , isSyncingVolume ]
242+ [ connectionStatus , resolveTargetDeviceId , executeSpotify ]
237243 )
238244
239- const debounceTimeoutRef = useRef < number | null > ( null )
245+ const handleVolumeChangeCommitted = useCallback (
246+ ( val : number ) => {
247+ setIsSliding ( false )
248+ sendVolumeCommand ( val )
249+ } ,
250+ [ sendVolumeCommand ]
251+ )
240252
241253 useEffect ( ( ) => {
242254 if ( connectionStatus !== 'Connected' ) {
243255 lastSentVolumeRef . current = null
244256 }
245257 } , [ connectionStatus ] )
246258
247- useEffect ( ( ) => {
248- // Clear any existing timer
249- if ( debounceTimeoutRef . current ) {
250- window . clearTimeout ( debounceTimeoutRef . current )
251- }
252-
253- // Set a new timer to send the volume command after 300ms
254- debounceTimeoutRef . current = window . setTimeout ( ( ) => {
255- sendVolumeCommand ( volume )
256- } , 300 )
257-
258- // Cleanup function to clear the timeout if the component unmounts
259- // or if the volume changes again before the timeout has passed
260- return ( ) => {
261- if ( debounceTimeoutRef . current ) {
262- window . clearTimeout ( debounceTimeoutRef . current )
263- }
264- if ( volumeLockTimeoutRef . current ) {
265- clearTimeout ( volumeLockTimeoutRef . current )
266- }
267- }
268- } , [ volume , sendVolumeCommand ] )
269-
270259 return (
271260 < ControlCard
272261 data-testid = "spotify-controls"
@@ -306,26 +295,20 @@ const SpotifyControls = () => {
306295 justifyContent : 'center' ,
307296 } }
308297 >
309- { player && ! isReady ? (
310- < Typography variant = "body2" sx = { { color : 'orange' } } >
311- Registering HRM Web Player...
298+ < >
299+ < Typography
300+ variant = "subtitle1"
301+ sx = { { fontWeight : 'medium' , lineHeight : 1.2 } }
302+ >
303+ { spotifyData . playback . track . name }
312304 </ Typography >
313- ) : (
314- < >
315- < Typography
316- variant = "subtitle1"
317- sx = { { fontWeight : 'medium' , lineHeight : 1.2 } }
318- >
319- { spotifyData . playback . track . name }
320- </ Typography >
321- < Typography
322- variant = "body2"
323- sx = { { color : 'grey.400' , lineHeight : 1.2 } }
324- >
325- { spotifyData . playback . track . artist }
326- </ Typography >
327- </ >
328- ) }
305+ < Typography
306+ variant = "body2"
307+ sx = { { color : 'grey.400' , lineHeight : 1.2 } }
308+ >
309+ { spotifyData . playback . track . artist }
310+ </ Typography >
311+ </ >
329312 </ Box >
330313
331314 < PlaybackControls
@@ -338,6 +321,7 @@ const SpotifyControls = () => {
338321 volume = { volume }
339322 muted = { muted }
340323 onVolumeChange = { handleVolumeChange }
324+ onVolumeChangeCommitted = { handleVolumeChangeCommitted }
341325 onToggleMute = { toggleMute }
342326 showValue = { true }
343327 />
0 commit comments