feat(03-02): add comprehensive error handling with user feedback
Implemented error state management and user-facing error messages: Error handling utilities: - handleError(type, message, err) - Sets error state with type classification - clearError() - Clears error state - retryOperation() - Placeholder for retry logic Error types: - 'file' - Failed to load track duration, invalid files - 'network' - Lost connection after 3 consecutive polling failures - 'playback' - Failed playback operations (play/stop/seek/volume/loop) - 'general' - Audio engine not available (null jamClient) Enhanced error handling in: - fetchDuration: Checks for jamClient, validates duration, handles failures - Polling interval: Tracks consecutive errors, stops after 3 failures - handlePlay: Checks jamClient, reverts isPlaying on error - handleStop: Checks jamClient, force resets UI on error - handleVolumeChange: Reverts volume slider on error - handleLoopChange: Reverts checkbox on error - handleSeek: Reverts position on error Error UI: - Error banner with color coding (red for critical, yellow for warning) - Shows error message with dismiss button - Retry button for network/file errors - Added to both popup and modal versions
This commit is contained in:
parent
fcf6afb7be
commit
acbddd35e3
|
|
@ -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 = ({
|
|||
<span>{getFileName(backingTrack)}</span>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
backgroundColor: errorType === 'file' || errorType === 'network' ? '#f8d7da' : '#fff3cd',
|
||||
color: errorType === 'file' || errorType === 'network' ? '#721c24' : '#856404',
|
||||
border: `1px solid ${errorType === 'file' || errorType === 'network' ? '#f5c6cb' : '#ffeaa7'}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div>{error}</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<Button size="sm" color="secondary" onClick={clearError}>
|
||||
Dismiss
|
||||
</Button>
|
||||
{(errorType === 'network' || errorType === 'file') && (
|
||||
<Button size="sm" color="primary" onClick={retryOperation}>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Controls */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }}>
|
||||
|
|
@ -487,6 +611,32 @@ const JKSessionBackingTrackPlayer = ({
|
|||
<span>{getFileName(backingTrack)}</span>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
backgroundColor: errorType === 'file' || errorType === 'network' ? '#f8d7da' : '#fff3cd',
|
||||
color: errorType === 'file' || errorType === 'network' ? '#721c24' : '#856404',
|
||||
border: `1px solid ${errorType === 'file' || errorType === 'network' ? '#f5c6cb' : '#ffeaa7'}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div>{error}</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<Button size="sm" color="secondary" onClick={clearError}>
|
||||
Dismiss
|
||||
</Button>
|
||||
{(errorType === 'network' || errorType === 'file') && (
|
||||
<Button size="sm" color="primary" onClick={retryOperation}>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Controls */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue