diff --git a/jam-ui/src/store/features/mediaSlice.js b/jam-ui/src/store/features/mediaSlice.js index a017daa0d..d12ac224e 100644 --- a/jam-ui/src/store/features/mediaSlice.js +++ b/jam-ui/src/store/features/mediaSlice.js @@ -17,16 +17,18 @@ export const loadJamTrack = createAsyncThunk( 'media/loadJamTrack', async ({ jamTrack, jamClient }, { rejectWithValue }) => { try { + // Build fqId for all JamTrack calls (format: {jamTrackId}-{sampleRate}) + const sampleRate = await jamClient.GetSampleRate(); + const sampleRateForFilename = sampleRate === 48 ? '48' : '44'; + const fqId = `${jamTrack.id}-${sampleRateForFilename}`; + // Load JMep data if available (matches MediaContext:163-170 logic) if (jamTrack.jmep) { - const sampleRate = await jamClient.GetSampleRate(); - const sampleRateForFilename = sampleRate === 48 ? '48' : '44'; - const fqId = `${jamTrack.id}-${sampleRateForFilename}`; await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep); } // Play/load the jamtrack - const result = await jamClient.JamTrackPlay(jamTrack.id); + const result = await jamClient.JamTrackPlay(fqId); if (!result) { throw new Error('Unable to open JamTrack'); @@ -59,8 +61,27 @@ const initialState = { recordedTracks: [], // [{ recordingName, isOpener, userName, instrumentIcon, track, mixers }] // JamTrack state (real-time playback state from native client) - jamTrackState: {}, // Real-time jamTrack playback state from WebSocket JAM_TRACK_CHANGES - downloadingJamTrack: false, + jamTrackState: { + isPlaying: false, + isPaused: false, + currentPositionMs: 0, + durationMs: 0, + selectedMixdownId: null, + playbackMode: null, // 'master' | 'custom-mix' | 'stem' + lastUpdate: null + }, + + // Download/sync state machine + downloadState: { + jamTrackId: null, + mixdownId: null, + fqId: null, + state: 'idle', // 'idle' | 'checking' | 'downloading' | 'keying' | 'synchronized' | 'error' + progress: 0, + currentStep: 0, + totalSteps: 0, + error: null + }, // Loading states for async operations loading: { @@ -94,8 +115,38 @@ export const mediaSlice = createSlice({ state.jamTrackState = { ...state.jamTrackState, ...action.payload }; }, + setJamTrackState: (state, action) => { + state.jamTrackState = action.payload; + }, + clearJamTrackState: (state) => { - state.jamTrackState = {}; + state.jamTrackState = { + isPlaying: false, + isPaused: false, + currentPositionMs: 0, + durationMs: 0, + selectedMixdownId: null, + playbackMode: null, + lastUpdate: null + }; + }, + + // Download state management + setDownloadState: (state, action) => { + state.downloadState = { ...state.downloadState, ...action.payload }; + }, + + clearDownloadState: (state) => { + state.downloadState = { + jamTrackId: null, + mixdownId: null, + fqId: null, + state: 'idle', + progress: 0, + currentStep: 0, + totalSteps: 0, + error: null + }; }, // Clear all media (called on session end or media close) @@ -103,8 +154,25 @@ export const mediaSlice = createSlice({ state.backingTracks = []; state.jamTracks = []; state.recordedTracks = []; - state.jamTrackState = {}; - state.downloadingJamTrack = false; + state.jamTrackState = { + isPlaying: false, + isPaused: false, + currentPositionMs: 0, + durationMs: 0, + selectedMixdownId: null, + playbackMode: null, + lastUpdate: null + }; + state.downloadState = { + jamTrackId: null, + mixdownId: null, + fqId: null, + state: 'idle', + progress: 0, + currentStep: 0, + totalSteps: 0, + error: null + }; state.error = null; } }, @@ -129,17 +197,18 @@ export const mediaSlice = createSlice({ // Load jam track .addCase(loadJamTrack.pending, (state) => { state.loading.jamTrack = true; - state.downloadingJamTrack = true; + state.downloadState.state = 'checking'; state.error = null; }) .addCase(loadJamTrack.fulfilled, (state) => { state.loading.jamTrack = false; - state.downloadingJamTrack = false; + state.downloadState.state = 'synchronized'; // Note: jamTracks array updated by MIXER_CHANGES WebSocket message }) .addCase(loadJamTrack.rejected, (state, action) => { state.loading.jamTrack = false; - state.downloadingJamTrack = false; + state.downloadState.state = 'error'; + state.downloadState.error = { message: action.payload }; state.error = action.payload; }) @@ -153,7 +222,25 @@ export const mediaSlice = createSlice({ state.backingTracks = []; state.jamTracks = []; state.recordedTracks = []; - state.jamTrackState = {}; + state.jamTrackState = { + isPlaying: false, + isPaused: false, + currentPositionMs: 0, + durationMs: 0, + selectedMixdownId: null, + playbackMode: null, + lastUpdate: null + }; + state.downloadState = { + jamTrackId: null, + mixdownId: null, + fqId: null, + state: 'idle', + progress: 0, + currentStep: 0, + totalSteps: 0, + error: null + }; }) .addCase(closeMedia.rejected, (state, action) => { state.loading.closing = false; @@ -167,7 +254,10 @@ export const { setJamTracks, setRecordedTracks, updateJamTrackState, + setJamTrackState, clearJamTrackState, + setDownloadState, + clearDownloadState, clearAllMedia } = mediaSlice.actions; @@ -178,6 +268,6 @@ export const selectBackingTracks = (state) => state.media.backingTracks; export const selectJamTracks = (state) => state.media.jamTracks; export const selectRecordedTracks = (state) => state.media.recordedTracks; export const selectJamTrackState = (state) => state.media.jamTrackState; -export const selectDownloadingJamTrack = (state) => state.media.downloadingJamTrack; +export const selectDownloadState = (state) => state.media.downloadState; export const selectMediaLoading = (state) => state.media.loading; export const selectMediaError = (state) => state.media.error;