From 217005c94b88494e82bb18cfc0efe5f0b28fb12f Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 15 Jan 2026 00:48:06 +0530 Subject: [PATCH] feat(05-03): implement playback control handlers (play/pause/stop) - Add handlePlay with loadJamTrack integration and pause-resume logic - Add handlePause for pausing playback - Add handleStop for stopping and resetting position - Implement UAT-003 fix pattern (pendingSeekRef for pause-seek-resume) - Add isOperating flag to prevent rapid clicks - Render playback buttons with proper disabled states - Error handling with typed errors (file/network red, playback yellow) Co-Authored-By: Claude Sonnet 4.5 --- .../client/JKSessionJamTrackPlayer.js | 115 ++++++++++++++++-- 1 file changed, 108 insertions(+), 7 deletions(-) diff --git a/jam-ui/src/components/client/JKSessionJamTrackPlayer.js b/jam-ui/src/components/client/JKSessionJamTrackPlayer.js index 818fe134e..e86d1b75b 100644 --- a/jam-ui/src/components/client/JKSessionJamTrackPlayer.js +++ b/jam-ui/src/components/client/JKSessionJamTrackPlayer.js @@ -34,6 +34,7 @@ const JKSessionJamTrackPlayer = ({ // Refs const fqIdRef = useRef(null); const mountedRef = useRef(true); + const pendingSeekRef = useRef(null); // Helper: Build fqId const buildFqId = useCallback(async () => { @@ -102,18 +103,118 @@ const JKSessionJamTrackPlayer = ({ }; }, [dispatch, jamClient]); - // Placeholder render (will be filled in Plan 3) + // Playback control handlers + const handlePlay = useCallback(async () => { + if (isOperating || !jamClient || !fqIdRef.current) return; + + try { + setIsOperating(true); + setError(null); + + // If not playing yet, load JamTrack + if (!jamTrackState.isPlaying) { + await dispatch(loadJamTrack({ + jamTrack, + mixdownId: selectedMixdownId, + autoPlay: true, + jamClient + })).unwrap(); + } else if (jamTrackState.isPaused) { + // Resume from pause + await jamClient.JamTrackResume(fqIdRef.current); + + // Apply pending seek if exists (UAT-003 fix) + if (pendingSeekRef.current !== null) { + await jamClient.JamTrackSeekMs(fqIdRef.current, pendingSeekRef.current); + pendingSeekRef.current = null; + } + + dispatch(setJamTrackState({ isPaused: false, isPlaying: true })); + } + } catch (err) { + console.error('[JamTrack] Play error:', err); + setError({ type: 'playback', message: 'Failed to play JamTrack' }); + } finally { + if (mountedRef.current) { + setIsOperating(false); + } + } + }, [isOperating, jamClient, jamTrack, jamTrackState, selectedMixdownId, dispatch]); + + const handlePause = useCallback(async () => { + if (isOperating || !jamClient || !fqIdRef.current) return; + + try { + setIsOperating(true); + setError(null); + + await jamClient.JamTrackPause(fqIdRef.current); + dispatch(setJamTrackState({ isPaused: true, isPlaying: false })); + } catch (err) { + console.error('[JamTrack] Pause error:', err); + setError({ type: 'playback', message: 'Failed to pause JamTrack' }); + } finally { + if (mountedRef.current) { + setIsOperating(false); + } + } + }, [isOperating, jamClient, dispatch]); + + const handleStop = useCallback(async () => { + if (isOperating || !jamClient || !fqIdRef.current) return; + + try { + setIsOperating(true); + setError(null); + + await jamClient.JamTrackStop(fqIdRef.current); + dispatch(setJamTrackState({ + isPlaying: false, + isPaused: false, + currentPositionMs: 0 + })); + } catch (err) { + console.error('[JamTrack] Stop error:', err); + setError({ type: 'playback', message: 'Failed to stop JamTrack' }); + } finally { + if (mountedRef.current) { + setIsOperating(false); + } + } + }, [isOperating, jamClient, dispatch]); + if (!isOpen && !isPopup) return null; return (
-

JamTrack Player (Skeleton)

- {isLoadingSync &&

Checking sync status...

} - {error &&

{error.message}

} - {downloadState.state !== 'idle' && ( -

Download State: {downloadState.state} ({downloadState.progress}%)

+ {error && ( +
+ {error.message} + +
)} -

Ready for playback controls (Plan 3)

+ + {downloadState.state !== 'idle' && downloadState.state !== 'synchronized' && ( +
+

Download: {downloadState.state} ({downloadState.progress}%)

+
+ )} + +
+ + + +
+ +
+

Position: {jamTrackState.currentPositionMs}ms / Duration: {jamTrackState.durationMs}ms

+
); };