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:
parent
c4b2a123bd
commit
217005c94b
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue