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 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-15 00:48:06 +05:30
parent c4b2a123bd
commit 217005c94b
1 changed files with 108 additions and 7 deletions

View File

@ -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 (
<div className="jamtrack-player">
<h3>JamTrack Player (Skeleton)</h3>
{isLoadingSync && <p>Checking sync status...</p>}
{error && <p style={{ color: 'red' }}>{error.message}</p>}
{downloadState.state !== 'idle' && (
<p>Download State: {downloadState.state} ({downloadState.progress}%)</p>
{error && (
<div style={{ background: error.type === 'file' || error.type === 'network' ? '#fee' : '#ffd', padding: '10px' }}>
{error.message}
<button onClick={() => setError(null)}>Dismiss</button>
</div>
)}
<p>Ready for playback controls (Plan 3)</p>
{downloadState.state !== 'idle' && downloadState.state !== 'synchronized' && (
<div>
<p>Download: {downloadState.state} ({downloadState.progress}%)</p>
</div>
)}
<div>
<button onClick={handlePlay} disabled={isOperating || isLoadingSync}>
{jamTrackState.isPaused ? 'Resume' : 'Play'}
</button>
<button onClick={handlePause} disabled={isOperating || !jamTrackState.isPlaying}>
Pause
</button>
<button onClick={handleStop} disabled={isOperating || (!jamTrackState.isPlaying && !jamTrackState.isPaused)}>
Stop
</button>
</div>
<div>
<p>Position: {jamTrackState.currentPositionMs}ms / Duration: {jamTrackState.durationMs}ms</p>
</div>
</div>
);
};