diff --git a/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js b/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js index b8c6c66a7..d9972a503 100644 --- a/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js +++ b/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js @@ -30,6 +30,10 @@ const JKSessionBackingTrackPlayer = ({ const [error, setError] = useState(null); const [errorType, setErrorType] = useState(null); // 'file', 'network', 'playback', 'general' + // Loading states + const [isLoadingDuration, setIsLoadingDuration] = useState(true); + const [isOperating, setIsOperating] = useState(false); // Operation in progress + const volumeRef = useRef(null); const trackVolumeObjectRef = useRef({ volL: 0, @@ -84,9 +88,11 @@ const JKSessionBackingTrackPlayer = ({ // Fetch and set duration immediately when track loads (async) const fetchDuration = async () => { + setIsLoadingDuration(true); try { if (!jamClient) { handleError('general', 'Audio engine not available', null); + setIsLoadingDuration(false); return; } @@ -101,16 +107,19 @@ const JKSessionBackingTrackPlayer = ({ handleError('file', 'Failed to load track duration. The file may be invalid or unsupported.', null); setDuration('0:00'); setDurationMs(0); + setIsLoadingDuration(false); return; } setDurationMs(validDuration); setDuration(formatTime(validDuration)); clearError(); // Clear any previous errors + setIsLoadingDuration(false); } catch (error) { handleError('file', 'Failed to load track duration', error); setDuration('0:00'); setDurationMs(0); + setIsLoadingDuration(false); } }; @@ -183,6 +192,9 @@ const JKSessionBackingTrackPlayer = ({ }, [isPlaying, jamClient, backingTrack]); const handlePlay = async () => { + if (isOperating) return; // Prevent rapid clicks + setIsOperating(true); + try { if (!jamClient) { handleError('general', 'Audio engine not available', null); @@ -248,10 +260,15 @@ const JKSessionBackingTrackPlayer = ({ handleError('playback', 'Failed to start playback', error); // Reset isPlaying to ensure UI is consistent setIsPlaying(false); + } finally { + setIsOperating(false); } }; const handleStop = async () => { + if (isOperating) return; // Prevent rapid clicks + setIsOperating(true); + try { if (!jamClient) { handleError('general', 'Audio engine not available', null); @@ -273,6 +290,8 @@ const JKSessionBackingTrackPlayer = ({ setCurrentTime('0:00'); setCurrentPositionMs(0); pendingSeekPositionRef.current = null; + } finally { + setIsOperating(false); } }; @@ -371,6 +390,10 @@ const JKSessionBackingTrackPlayer = ({ const seekPositionMs = parseInt(e.target.value); const previousPosition = currentPositionMs; + // Update local state immediately for responsive UI (don't block on isOperating) + setCurrentPositionMs(seekPositionMs); + setCurrentTime(formatTime(seekPositionMs)); + try { if (!jamClient) { handleError('general', 'Audio engine not available', null); @@ -379,10 +402,6 @@ const JKSessionBackingTrackPlayer = ({ console.log('[BTP] handleSeek:', { seekPositionMs, isPlaying }); - // Update local state immediately for responsive UI - setCurrentPositionMs(seekPositionMs); - setCurrentTime(formatTime(seekPositionMs)); - // Seek the native client to the new position await jamClient.SessionTrackSeekMs(seekPositionMs); @@ -516,21 +535,29 @@ const JKSessionBackingTrackPlayer = ({ )} + {/* Loading indicator */} + {isLoadingDuration && ( +