diff --git a/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js b/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js index 13ecf6fe7..b8c6c66a7 100644 --- a/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js +++ b/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js @@ -26,6 +26,10 @@ const JKSessionBackingTrackPlayer = ({ const [currentPositionMs, setCurrentPositionMs] = useState(0); const [durationMs, setDurationMs] = useState(0); + // Error handling state + const [error, setError] = useState(null); + const [errorType, setErrorType] = useState(null); // 'file', 'network', 'playback', 'general' + const volumeRef = useRef(null); const trackVolumeObjectRef = useRef({ volL: 0, @@ -39,6 +43,9 @@ const JKSessionBackingTrackPlayer = ({ // UAT-003 Investigation: Track seek attempts while paused const pendingSeekPositionRef = useRef(null); + // Error tracking for polling + const consecutivePollingErrorsRef = useRef(0); + // Utility function to format milliseconds to M:SS format const formatTime = (ms) => { // Handle undefined, null, or NaN values @@ -51,6 +58,23 @@ const JKSessionBackingTrackPlayer = ({ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; }; + // Error handling utilities + const handleError = (type, message, err) => { + console.error(`[BTP] ${type}:`, message, err); + setError(message); + setErrorType(type); + }; + + const clearError = () => { + setError(null); + setErrorType(null); + }; + + const retryOperation = () => { + clearError(); + // Specific retry logic handled by individual operations + }; + useEffect(() => { if (isOpen && backingTrack && jamClient) { // Initialize player state when opened @@ -61,15 +85,30 @@ const JKSessionBackingTrackPlayer = ({ // Fetch and set duration immediately when track loads (async) const fetchDuration = async () => { try { + if (!jamClient) { + handleError('general', 'Audio engine not available', null); + return; + } + const durationInMs = await jamClient.SessionGetTracksPlayDurationMs(); console.log('[BTP] Duration from jamClient:', durationInMs, 'Type:', typeof durationInMs); // Convert string to number (jamClient returns string values) const validDuration = parseInt(durationInMs, 10) || 0; + + // Check for invalid duration (file error) + if (validDuration === 0 || isNaN(validDuration)) { + handleError('file', 'Failed to load track duration. The file may be invalid or unsupported.', null); + setDuration('0:00'); + setDurationMs(0); + return; + } + setDurationMs(validDuration); setDuration(formatTime(validDuration)); + clearError(); // Clear any previous errors } catch (error) { - console.error('Error fetching track duration:', error); + handleError('file', 'Failed to load track duration', error); setDuration('0:00'); setDurationMs(0); } @@ -116,8 +155,21 @@ const JKSessionBackingTrackPlayer = ({ console.log('[BTP] Playing state changed from', isPlaying, 'to', trackIsPlaying); setIsPlaying(trackIsPlaying); } + + // Reset error counter on successful poll + consecutivePollingErrorsRef.current = 0; } catch (error) { - console.error('Error polling playback status:', error); + console.error('[BTP] Error polling playback status:', error); + consecutivePollingErrorsRef.current += 1; + + // After 3 consecutive failures, show error and stop polling + if (consecutivePollingErrorsRef.current >= 3) { + handleError('network', 'Lost connection to audio engine', error); + setIsPlaying(false); + if (intervalId) { + clearInterval(intervalId); + } + } } }, 500); } @@ -132,6 +184,11 @@ const JKSessionBackingTrackPlayer = ({ const handlePlay = async () => { try { + if (!jamClient) { + handleError('general', 'Audio engine not available', null); + return; + } + console.log('[BTP] handlePlay:', { isPlaying, currentPositionMs, @@ -139,6 +196,8 @@ const JKSessionBackingTrackPlayer = ({ hasPendingSeek: pendingSeekPositionRef.current !== null }); + clearError(); // Clear any previous errors + if (isPlaying) { // Pause await jamClient.SessionPausePlay(); @@ -186,12 +245,19 @@ const JKSessionBackingTrackPlayer = ({ console.log('[BTP] SessionStartPlay completed, isPlaying set to true'); } } catch (error) { - console.error('Error toggling playback:', error); + handleError('playback', 'Failed to start playback', error); + // Reset isPlaying to ensure UI is consistent + setIsPlaying(false); } }; const handleStop = async () => { try { + if (!jamClient) { + handleError('general', 'Audio engine not available', null); + return; + } + await jamClient.SessionStopPlay(); setIsPlaying(false); setCurrentTime('0:00'); @@ -199,16 +265,29 @@ const JKSessionBackingTrackPlayer = ({ // UAT-003: Clear any pending seek position when stopping pendingSeekPositionRef.current = null; + clearError(); // Clear any previous errors } catch (error) { - console.error('Error stopping playback:', error); + handleError('playback', 'Failed to stop playback', error); + // Force reset UI state anyway + setIsPlaying(false); + setCurrentTime('0:00'); + setCurrentPositionMs(0); + pendingSeekPositionRef.current = null; } }; const handleVolumeChange = async (e) => { const newVolume = parseInt(e.target.value); + const previousVolume = volume; setVolume(newVolume); try { + if (!jamClient) { + handleError('general', 'Audio engine not available', null); + setVolume(previousVolume); // Revert to previous + return; + } + // Volume is controlled through the mixer system // Get the backing track mixer (use personal mixer) const backingTrackMixer = backingTrackMixers[0]?.mixers?.personal?.mixer; @@ -240,26 +319,36 @@ const JKSessionBackingTrackPlayer = ({ console.log('[BTP] Volume changed to', newVolume, '% (', volumeDb, 'dB)'); } catch (error) { - console.error('[BTP] Error setting volume:', error); + handleError('playback', 'Failed to adjust volume', error); + setVolume(previousVolume); // Revert to previous on error } }; const handleLoopChange = async (e) => { const shouldLoop = e.target.checked; + const previousLoop = isLooping; setIsLooping(shouldLoop); try { + if (!jamClient) { + handleError('general', 'Audio engine not available', null); + setIsLooping(previousLoop); // Revert checkbox + return; + } + // Set loop state using SessionSetBackingTrackFileLoop // This requires the file path and loop boolean if (!backingTrack || !backingTrack.path) { - console.warn('[BTP] No backing track path available for loop change'); + handleError('playback', 'No backing track available for loop change', null); + setIsLooping(previousLoop); // Revert checkbox return; } await jamClient.SessionSetBackingTrackFileLoop(backingTrack.path, shouldLoop); console.log('[BTP] Loop changed to', shouldLoop); } catch (error) { - console.error('[BTP] Error setting loop state:', error); + handleError('playback', 'Failed to toggle loop', error); + setIsLooping(previousLoop); // Revert checkbox on error } }; @@ -280,8 +369,14 @@ const JKSessionBackingTrackPlayer = ({ */ const handleSeek = async (e) => { const seekPositionMs = parseInt(e.target.value); + const previousPosition = currentPositionMs; try { + if (!jamClient) { + handleError('general', 'Audio engine not available', null); + return; + } + console.log('[BTP] handleSeek:', { seekPositionMs, isPlaying }); // Update local state immediately for responsive UI @@ -305,7 +400,10 @@ const JKSessionBackingTrackPlayer = ({ console.log('[BTP] Seek completed'); } catch (error) { - console.error('[BTP] Error seeking:', error); + handleError('playback', 'Failed to seek to position', error); + // Revert UI position to last known good + setCurrentPositionMs(previousPosition); + setCurrentTime(formatTime(previousPosition)); } }; @@ -392,6 +490,32 @@ const JKSessionBackingTrackPlayer = ({ {getFileName(backingTrack)} + {/* Error Display */} + {error && ( +
+
{error}
+
+ + {(errorType === 'network' || errorType === 'file') && ( + + )} +
+
+ )} + {/* Row 2: Controls */}
@@ -487,6 +611,32 @@ const JKSessionBackingTrackPlayer = ({ {getFileName(backingTrack)}
+ {/* Error Display */} + {error && ( +
+
{error}
+
+ + {(errorType === 'network' || errorType === 'file') && ( + + )} +
+
+ )} + {/* Row 2: Controls */}