679 lines
22 KiB
JavaScript
679 lines
22 KiB
JavaScript
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
|
import { enqueueMixdown } from '../../helpers/rest';
|
|
|
|
// Channel group IDs for track filtering (from globals.js ChannelGroupIds)
|
|
const MEDIA_TRACK_GROUP = 6; // ChannelGroupIds.MediaTrackGroup
|
|
|
|
// Async thunks for media actions
|
|
export const openBackingTrack = createAsyncThunk(
|
|
'media/openBackingTrack',
|
|
async ({ file, jamClient }, { rejectWithValue }) => {
|
|
try {
|
|
// Open the backing track file via native client
|
|
await jamClient.SessionOpenBackingTrackFile(file, false);
|
|
|
|
// Get track info from native client to populate Redux state
|
|
const allTracks = await jamClient.SessionGetAllControlState(true);
|
|
|
|
// Extract backing track data (same pattern as useTrackHelpers.getBackingTracks)
|
|
const backingTracks = [];
|
|
for (const track of allTracks) {
|
|
if (track.group_id === MEDIA_TRACK_GROUP &&
|
|
track.media_type === 'BackingTrack' &&
|
|
!track.managed) {
|
|
backingTracks.push({
|
|
id: track.persisted_track_id,
|
|
rid: track.rid,
|
|
filename: track.filename,
|
|
// Compute shortFilename from full path
|
|
shortFilename: track.filename ? track.filename.split('/').pop().split('\\').pop() : 'Audio File'
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log('[openBackingTrack] Got backing tracks from native client:', backingTracks);
|
|
|
|
return { file, backingTracks };
|
|
} catch (error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
}
|
|
);
|
|
|
|
export const loadJamTrack = createAsyncThunk(
|
|
'media/loadJamTrack',
|
|
async ({ jamTrack, mixdownId = null, autoPlay = false, jamClient, jamServer }, { dispatch, rejectWithValue }) => {
|
|
try {
|
|
// Build fqId
|
|
const sampleRate = await jamClient.GetSampleRate();
|
|
const fqId = `${jamTrack.id}-${sampleRate === 48 ? '48' : '44'}`;
|
|
|
|
// Check sync state
|
|
const trackDetail = await jamClient.JamTrackGetTrackDetail(fqId);
|
|
|
|
// If not synchronized, trigger download
|
|
if (!trackDetail || !trackDetail.key_state || trackDetail.key_state !== 'AVAILABLE') {
|
|
await dispatch(downloadJamTrack({ jamTrack, mixdownId, fqId, jamClient, jamServer })).unwrap();
|
|
}
|
|
|
|
// Load JMEP if present
|
|
if (jamTrack.jmep) {
|
|
await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
|
|
}
|
|
|
|
// Play with fqId (not jamTrack.id)
|
|
const result = await jamClient.JamTrackPlay(fqId);
|
|
if (!result) {
|
|
throw new Error('Unable to play JamTrack');
|
|
}
|
|
|
|
return { jamTrack, fqId, result };
|
|
} catch (error) {
|
|
return rejectWithValue(error.message || error);
|
|
}
|
|
}
|
|
);
|
|
|
|
export const downloadJamTrack = createAsyncThunk(
|
|
'media/downloadJamTrack',
|
|
async ({ jamTrack, mixdownId, fqId, jamClient, jamServer }, { dispatch, rejectWithValue, getState }) => {
|
|
try {
|
|
|
|
// Get client sample rate for package selection (pickMyPackage logic)
|
|
const rawSampleRate = await jamClient.GetSampleRate();
|
|
|
|
// Normalize sample rate (jamClient returns float like 44.099998474121094 or 48.0)
|
|
// Round to nearest standard rate: 44 or 48 kHz
|
|
const sampleRate = rawSampleRate >= 46 ? 48 : 44;
|
|
|
|
// Use mixdowns from jamTrack object (fetched from REST API)
|
|
// jamClient.JamTrackGetMixdowns returns a different structure without packages
|
|
const mixdowns = jamTrack.mixdowns;
|
|
|
|
if (!mixdowns || mixdowns.length === 0) {
|
|
throw new Error('No mixdowns available for this JamTrack');
|
|
}
|
|
|
|
// Find the mixdown (use selected mixdownId if provided, else use first)
|
|
const mixdown = mixdownId
|
|
? mixdowns.find(m => m.id === mixdownId) || mixdowns[0]
|
|
: mixdowns[0];
|
|
|
|
if (!mixdown) {
|
|
throw new Error('Selected mixdown not found');
|
|
}
|
|
|
|
if (!mixdown.packages || mixdown.packages.length === 0) {
|
|
throw new Error(`Mixdown "${mixdown.name}" has no packages available`);
|
|
}
|
|
|
|
// pickMyPackage logic with fallback strategy
|
|
// CRITICAL: Sample rate MUST match - native client cannot play mismatched rates
|
|
// Preference order for matching sample rate:
|
|
// 1. ogg + jkz encryption (ideal for security)
|
|
// 2. ogg + any/no encryption (preferred format)
|
|
// 3. any format (mp3, etc.)
|
|
|
|
let compatiblePackage = null;
|
|
|
|
// Try 1: Ideal package (ogg, jkz, matching sample rate)
|
|
compatiblePackage = mixdown.packages.find(pkg =>
|
|
pkg.file_type === 'ogg' &&
|
|
pkg.encrypt_type === 'jkz' &&
|
|
pkg.sample_rate === sampleRate
|
|
);
|
|
|
|
// Try 2: ogg with any encryption, matching sample rate
|
|
if (!compatiblePackage) {
|
|
compatiblePackage = mixdown.packages.find(pkg =>
|
|
pkg.file_type === 'ogg' &&
|
|
pkg.sample_rate === sampleRate
|
|
);
|
|
}
|
|
|
|
// Try 3: Any format with matching sample rate
|
|
if (!compatiblePackage) {
|
|
compatiblePackage = mixdown.packages.find(pkg =>
|
|
pkg.sample_rate === sampleRate
|
|
);
|
|
}
|
|
|
|
// No fallback to different sample rates - client cannot play mismatched rates
|
|
if (!compatiblePackage) {
|
|
const availableRates = mixdown.packages.map(p => p.sample_rate).join(', ');
|
|
throw new Error(`No package available for sample rate ${sampleRate}kHz. Available rates: ${availableRates}kHz. Try restarting your audio interface or selecting a different sample rate.`);
|
|
}
|
|
|
|
const packageId = compatiblePackage.id;
|
|
|
|
// ========== PACKAGING PHASE ==========
|
|
// Before downloading, package must be created/signed on server
|
|
// This matches legacy download_jamtrack.js.coffee behavior
|
|
|
|
dispatch(setDownloadState({
|
|
jamTrackId: jamTrack.id,
|
|
mixdownId,
|
|
fqId,
|
|
packageId,
|
|
state: 'packaging',
|
|
progress: 0,
|
|
packaging_steps: 0,
|
|
current_packaging_step: 0,
|
|
signing_state: null
|
|
}));
|
|
|
|
// Enqueue mixdown for server-side packaging
|
|
// Backend expects mixdown ID + package attributes, not package ID
|
|
console.log(`[JamTrack] Enqueueing mixdown ${mixdownId} for packaging (file_type: ${compatiblePackage.file_type}, encrypt_type: ${compatiblePackage.encrypt_type}, sample_rate: ${sampleRate})`);
|
|
try {
|
|
await enqueueMixdown({
|
|
id: mixdownId, // mixdown ID, not package ID
|
|
file_type: compatiblePackage.file_type,
|
|
encrypt_type: compatiblePackage.encrypt_type,
|
|
sample_rate: sampleRate
|
|
});
|
|
} catch (err) {
|
|
console.error('[JamTrack] Failed to enqueue mixdown:', err);
|
|
// Handle both Error objects and Response objects (from apiFetch)
|
|
const errorMessage = err?.message || err?.statusText || `HTTP ${err?.status}` || 'Unknown error';
|
|
throw new Error(`Failed to start packaging: ${errorMessage}`);
|
|
}
|
|
|
|
// Subscribe to WebSocket notifications for packaging progress
|
|
// WebSocket gateway will send SUBSCRIPTION_MESSAGE updates
|
|
// Handler in useSessionWebSocket.js will dispatch setDownloadState updates
|
|
console.log(`[JamTrack] Subscribing to packaging notifications for package ${packageId}`);
|
|
|
|
if (!jamServer || !jamServer.subscribe) {
|
|
throw new Error('WebSocket connection not available');
|
|
}
|
|
|
|
const subscribed = jamServer.subscribe('mixdown', packageId);
|
|
if (!subscribed) {
|
|
throw new Error('Failed to subscribe to packaging notifications');
|
|
}
|
|
|
|
// Cleanup function for unsubscribing
|
|
const unsubscribeFromPackaging = () => {
|
|
if (jamServer && jamServer.unsubscribe) {
|
|
jamServer.unsubscribe('mixdown', packageId);
|
|
}
|
|
};
|
|
|
|
// Wait for packaging to complete (signing_state === 'SIGNED')
|
|
// WebSocket notifications will update downloadState as packaging progresses
|
|
console.log('[JamTrack] Waiting for package to be signed by server...');
|
|
|
|
const waitForPackaging = () => {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
// Unsubscribe on timeout
|
|
unsubscribeFromPackaging();
|
|
reject(new Error('Packaging timeout after 60 seconds'));
|
|
}, 60000); // 60 second timeout
|
|
|
|
const checkInterval = setInterval(() => {
|
|
const state = getState();
|
|
const { signing_state, state: downloadStateValue } = state.media.downloadState;
|
|
|
|
// Check for success
|
|
if (signing_state === 'SIGNED') {
|
|
clearTimeout(timeout);
|
|
clearInterval(checkInterval);
|
|
console.log('[JamTrack] Package signed successfully, proceeding to download');
|
|
// Unsubscribe on success
|
|
unsubscribeFromPackaging();
|
|
resolve();
|
|
}
|
|
|
|
// Check for errors
|
|
if (signing_state === 'ERROR' || signing_state === 'SIGNING_TIMEOUT' ||
|
|
signing_state === 'QUEUED_TIMEOUT' || signing_state === 'QUIET_TIMEOUT') {
|
|
clearTimeout(timeout);
|
|
clearInterval(checkInterval);
|
|
// Unsubscribe on error
|
|
unsubscribeFromPackaging();
|
|
reject(new Error(`Packaging failed with state: ${signing_state}`));
|
|
}
|
|
|
|
// Check if download state shows error
|
|
if (downloadStateValue === 'error') {
|
|
clearTimeout(timeout);
|
|
clearInterval(checkInterval);
|
|
// Unsubscribe on error
|
|
unsubscribeFromPackaging();
|
|
reject(new Error('Packaging failed'));
|
|
}
|
|
}, 500); // Check every 500ms
|
|
});
|
|
};
|
|
|
|
await waitForPackaging();
|
|
|
|
// ========== DOWNLOAD PHASE ==========
|
|
// Package is now signed and ready to download
|
|
|
|
dispatch(setDownloadState({
|
|
state: 'downloading',
|
|
progress: 0
|
|
}));
|
|
|
|
// Set up global callbacks for native client
|
|
window.jamTrackDownloadProgress = (percent) => {
|
|
dispatch(setDownloadState({ progress: percent }));
|
|
};
|
|
|
|
window.jamTrackDownloadSuccess = () => {
|
|
dispatch(setDownloadState({ state: 'keying' }));
|
|
};
|
|
|
|
window.jamTrackDownloadFail = (error) => {
|
|
dispatch(setDownloadState({ state: 'error', error: { type: 'download', message: error } }));
|
|
};
|
|
|
|
// Initiate download (callbacks as string names)
|
|
await jamClient.JamTrackDownload(
|
|
jamTrack.id,
|
|
packageId,
|
|
jamTrack.userId,
|
|
'jamTrackDownloadProgress',
|
|
'jamTrackDownloadSuccess',
|
|
'jamTrackDownloadFail'
|
|
);
|
|
|
|
// Request keys
|
|
await jamClient.JamTrackKeysRequest();
|
|
|
|
dispatch(setDownloadState({ state: 'synchronized' }));
|
|
return { jamTrackId: jamTrack.id, fqId };
|
|
} catch (error) {
|
|
dispatch(setDownloadState({ state: 'error', error: { type: 'download', message: error.message } }));
|
|
return rejectWithValue(error.message || error);
|
|
}
|
|
}
|
|
);
|
|
|
|
export const checkJamTrackSync = createAsyncThunk(
|
|
'media/checkJamTrackSync',
|
|
async ({ jamTrack, jamClient }, { rejectWithValue }) => {
|
|
try {
|
|
const sampleRate = await jamClient.GetSampleRate();
|
|
const fqId = `${jamTrack.id}-${sampleRate === 48 ? '48' : '44'}`;
|
|
const trackDetail = await jamClient.JamTrackGetTrackDetail(fqId);
|
|
return { fqId, trackDetail, isSynchronized: trackDetail && trackDetail.key_state === 'AVAILABLE' };
|
|
} catch (error) {
|
|
return rejectWithValue(error.message || error);
|
|
}
|
|
}
|
|
);
|
|
|
|
export const loadJMEP = createAsyncThunk(
|
|
'media/loadJMEP',
|
|
async ({ jamTrack, fqId, jamClient }, { rejectWithValue }) => {
|
|
try {
|
|
if (!jamTrack.jmep) {
|
|
return { loaded: false };
|
|
}
|
|
await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
|
|
return { loaded: true, fqId };
|
|
} catch (error) {
|
|
return rejectWithValue(error.message || error);
|
|
}
|
|
}
|
|
);
|
|
|
|
export const seekJamTrack = createAsyncThunk(
|
|
'media/seekJamTrack',
|
|
async ({ fqId, positionMs, jamClient }, { getState, rejectWithValue }) => {
|
|
try {
|
|
// Apply UAT-003 fix pattern: if paused, store pending seek
|
|
const { media } = getState();
|
|
if (media.jamTrackState.isPaused) {
|
|
// Store pending seek, will be applied on resume
|
|
return { pendingSeek: positionMs };
|
|
}
|
|
|
|
await jamClient.JamTrackSeekMs(fqId, positionMs);
|
|
return { positionMs };
|
|
} catch (error) {
|
|
return rejectWithValue(error.message || error);
|
|
}
|
|
}
|
|
);
|
|
|
|
export const closeJamTrack = createAsyncThunk(
|
|
'media/closeJamTrack',
|
|
async ({ fqId, jamClient }, { dispatch, rejectWithValue }) => {
|
|
try {
|
|
// Stop playback if playing
|
|
const positionStr = await jamClient.JamTrackGetPositionMs(fqId);
|
|
const position = parseInt(positionStr, 10);
|
|
if (position > 0) {
|
|
await jamClient.JamTrackStop(fqId);
|
|
}
|
|
|
|
// Clear Redux state
|
|
dispatch(clearJamTrackState());
|
|
dispatch(clearDownloadState());
|
|
|
|
return { closed: true };
|
|
} catch (error) {
|
|
return rejectWithValue(error.message || error);
|
|
}
|
|
}
|
|
);
|
|
|
|
export const closeMedia = createAsyncThunk(
|
|
'media/closeMedia',
|
|
async ({ force = false, jamClient }, { rejectWithValue }) => {
|
|
try {
|
|
await jamClient.SessionCloseMedia(force);
|
|
return null;
|
|
} catch (error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
}
|
|
);
|
|
|
|
const initialState = {
|
|
// Media arrays (resolved/enriched data from mixer system)
|
|
// Format matches useMixerHelper resolved data structures
|
|
backingTracks: [], // [{ isOpener, shortFilename, instrumentIcon, photoUrl, showLoop, track, mixers }]
|
|
jamTracks: [], // [{ name, trackName, part, isOpener, instrumentIcon, track, mixers }]
|
|
recordedTracks: [], // [{ recordingName, isOpener, userName, instrumentIcon, track, mixers }]
|
|
|
|
// JamTrack state (real-time playback state from native client)
|
|
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,
|
|
packageId: null, // Package being packaged/downloaded
|
|
state: 'idle', // 'idle' | 'checking' | 'packaging' | 'downloading' | 'keying' | 'synchronized' | 'error'
|
|
progress: 0,
|
|
currentStep: 0,
|
|
totalSteps: 0,
|
|
// Packaging-specific fields (from legacy JamTrackStore)
|
|
packaging_steps: 0, // Total packaging steps
|
|
current_packaging_step: 0, // Current step in packaging
|
|
signing_state: null, // 'SIGNING_TIMEOUT' | 'QUEUED_TIMEOUT' | 'QUIET_TIMEOUT' | 'ERROR' | 'SIGNED' | null
|
|
error: null
|
|
},
|
|
|
|
// Loading states for async operations
|
|
loading: {
|
|
backingTrack: false,
|
|
jamTrack: false,
|
|
closing: false
|
|
},
|
|
|
|
error: null
|
|
};
|
|
|
|
export const mediaSlice = createSlice({
|
|
name: 'media',
|
|
initialState,
|
|
reducers: {
|
|
// Set resolved media data (called from useMixerHelper after processing)
|
|
setBackingTracks: (state, action) => {
|
|
state.backingTracks = action.payload;
|
|
},
|
|
|
|
setJamTracks: (state, action) => {
|
|
state.jamTracks = action.payload;
|
|
},
|
|
|
|
setRecordedTracks: (state, action) => {
|
|
state.recordedTracks = action.payload;
|
|
},
|
|
|
|
// JamTrack state updates (from WebSocket JAM_TRACK_CHANGES messages)
|
|
updateJamTrackState: (state, action) => {
|
|
state.jamTrackState = { ...state.jamTrackState, ...action.payload };
|
|
},
|
|
|
|
setJamTrackState: (state, action) => {
|
|
state.jamTrackState = action.payload;
|
|
},
|
|
|
|
clearJamTrackState: (state) => {
|
|
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,
|
|
packageId: null,
|
|
state: 'idle',
|
|
progress: 0,
|
|
currentStep: 0,
|
|
totalSteps: 0,
|
|
packaging_steps: 0,
|
|
current_packaging_step: 0,
|
|
signing_state: null,
|
|
error: null
|
|
};
|
|
},
|
|
|
|
// Clear all media (called on session end or media close)
|
|
clearAllMedia: (state) => {
|
|
state.backingTracks = [];
|
|
state.jamTracks = [];
|
|
state.recordedTracks = [];
|
|
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;
|
|
}
|
|
},
|
|
|
|
extraReducers: (builder) => {
|
|
builder
|
|
// Open backing track
|
|
.addCase(openBackingTrack.pending, (state) => {
|
|
state.loading.backingTrack = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(openBackingTrack.fulfilled, (state) => {
|
|
state.loading.backingTrack = false;
|
|
// Note: backingTracks array updated by MIXER_CHANGES WebSocket message
|
|
// which triggers useMixerHelper to resolve and dispatch setBackingTracks
|
|
})
|
|
.addCase(openBackingTrack.rejected, (state, action) => {
|
|
state.loading.backingTrack = false;
|
|
state.error = action.payload;
|
|
})
|
|
|
|
// Load jam track
|
|
.addCase(loadJamTrack.pending, (state) => {
|
|
state.loading.jamTrack = true;
|
|
state.downloadState.state = 'checking';
|
|
state.error = null;
|
|
})
|
|
.addCase(loadJamTrack.fulfilled, (state) => {
|
|
state.loading.jamTrack = false;
|
|
state.downloadState.state = 'synchronized';
|
|
// Note: jamTracks array updated by MIXER_CHANGES WebSocket message
|
|
})
|
|
.addCase(loadJamTrack.rejected, (state, action) => {
|
|
state.loading.jamTrack = false;
|
|
state.downloadState.state = 'error';
|
|
state.downloadState.error = { message: action.payload };
|
|
state.error = action.payload;
|
|
})
|
|
|
|
// Download jam track
|
|
.addCase(downloadJamTrack.pending, (state) => {
|
|
state.loading.jamTrack = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(downloadJamTrack.fulfilled, (state) => {
|
|
state.loading.jamTrack = false;
|
|
})
|
|
.addCase(downloadJamTrack.rejected, (state, action) => {
|
|
state.loading.jamTrack = false;
|
|
state.error = action.payload;
|
|
})
|
|
|
|
// Check jam track sync
|
|
.addCase(checkJamTrackSync.pending, (state) => {
|
|
state.downloadState.state = 'checking';
|
|
state.error = null;
|
|
})
|
|
.addCase(checkJamTrackSync.fulfilled, (state, action) => {
|
|
state.downloadState.state = action.payload.isSynchronized ? 'synchronized' : 'idle';
|
|
})
|
|
.addCase(checkJamTrackSync.rejected, (state, action) => {
|
|
state.downloadState.state = 'error';
|
|
state.downloadState.error = { message: action.payload };
|
|
state.error = action.payload;
|
|
})
|
|
|
|
// Load JMEP
|
|
.addCase(loadJMEP.pending, (state) => {
|
|
state.error = null;
|
|
})
|
|
.addCase(loadJMEP.fulfilled, (state) => {
|
|
// JMEP loaded successfully
|
|
})
|
|
.addCase(loadJMEP.rejected, (state, action) => {
|
|
state.error = action.payload;
|
|
})
|
|
|
|
// Seek jam track
|
|
.addCase(seekJamTrack.pending, (state) => {
|
|
state.error = null;
|
|
})
|
|
.addCase(seekJamTrack.fulfilled, (state, action) => {
|
|
if (action.payload.positionMs !== undefined) {
|
|
state.jamTrackState.currentPositionMs = action.payload.positionMs;
|
|
}
|
|
})
|
|
.addCase(seekJamTrack.rejected, (state, action) => {
|
|
state.error = action.payload;
|
|
})
|
|
|
|
// Close jam track
|
|
.addCase(closeJamTrack.pending, (state) => {
|
|
state.loading.closing = true;
|
|
})
|
|
.addCase(closeJamTrack.fulfilled, (state) => {
|
|
state.loading.closing = false;
|
|
})
|
|
.addCase(closeJamTrack.rejected, (state, action) => {
|
|
state.loading.closing = false;
|
|
state.error = action.payload;
|
|
})
|
|
|
|
// Close media
|
|
.addCase(closeMedia.pending, (state) => {
|
|
state.loading.closing = true;
|
|
})
|
|
.addCase(closeMedia.fulfilled, (state) => {
|
|
state.loading.closing = false;
|
|
// Clear all media data
|
|
state.backingTracks = [];
|
|
state.jamTracks = [];
|
|
state.recordedTracks = [];
|
|
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;
|
|
state.error = action.payload;
|
|
});
|
|
}
|
|
});
|
|
|
|
export const {
|
|
setBackingTracks,
|
|
setJamTracks,
|
|
setRecordedTracks,
|
|
updateJamTrackState,
|
|
setJamTrackState,
|
|
clearJamTrackState,
|
|
setDownloadState,
|
|
clearDownloadState,
|
|
clearAllMedia
|
|
} = mediaSlice.actions;
|
|
|
|
export default mediaSlice.reducer;
|
|
|
|
// Cleanup function for JamTrack download callbacks
|
|
// Called on component unmount to prevent memory leaks from orphaned window globals
|
|
// Pattern matches useRecordingHelpers.js cleanup approach
|
|
export const cleanupJamTrackCallbacks = () => {
|
|
if (typeof window !== 'undefined') {
|
|
delete window.jamTrackDownloadProgress;
|
|
delete window.jamTrackDownloadSuccess;
|
|
delete window.jamTrackDownloadFail;
|
|
}
|
|
};
|
|
|
|
// Selectors
|
|
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 selectDownloadState = (state) => state.media.downloadState;
|
|
export const selectMediaLoading = (state) => state.media.loading;
|
|
export const selectMediaError = (state) => state.media.error;
|