docs(v1.6): create roadmap for Media Features Polish
- 3 phases: JamTrack Polish, Backing Track Sync, Metronome Responsiveness - 8 requirements mapped to phases - Research complete for all features Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
daaa37a84e
commit
eab0b0d19a
|
|
@ -0,0 +1,59 @@
|
|||
# Requirements: v1.6 Media Features Polish
|
||||
|
||||
**Defined:** 2026-02-25
|
||||
**Core Value:** Fix usability issues in Metronome, Backing Track, and JamTrack features
|
||||
|
||||
## v1.6 Requirements
|
||||
|
||||
Requirements for polishing media player features. Each maps to roadmap phases.
|
||||
|
||||
### JamTrack Fixes
|
||||
|
||||
- [ ] **JT-01**: Defer stem/player UI rendering until backend processing completes
|
||||
- [ ] **JT-02**: Fix WindowPortal sizing to match player content (460x350)
|
||||
- [ ] **JT-03**: Implement "Create custom mix" navigation to `/jamtracks/{id}` in new tab
|
||||
- [ ] **JT-04**: Add callback cleanup for window.jamTrackDownload* in mediaSlice
|
||||
|
||||
### Backing Track Fixes
|
||||
|
||||
- [ ] **BT-01**: Use openBackingTrack() action to enable track sync to session screen
|
||||
- [ ] **BT-02**: Add ignore flag to duration fetch useEffect
|
||||
|
||||
### Metronome Fixes
|
||||
|
||||
- [ ] **MET-01**: Add debounce (300ms) to BPM/sound/meter control handlers
|
||||
- [ ] **MET-02**: Add timer cleanup for debounce timeouts on unmount
|
||||
|
||||
## Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Keep WindowPortal for Metronome | Consistency with JamTrack, BackingTrack, Chat. "about:blank" URL is browser security limitation. |
|
||||
| Include all performance fixes | Prevents memory leaks and state-after-unmount warnings |
|
||||
| 300ms debounce | Standard for responsive UI while avoiding excessive native client calls |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Explicitly excluded for this milestone.
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| Hide "about:blank" URL bar | Browser security constraint, cannot be changed |
|
||||
| Recording improvements | Separate milestone (v1.5 shipped) |
|
||||
| Additional media features | Keep v1.6 focused on polish |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| JT-01 | Phase 26 | Pending |
|
||||
| JT-02 | Phase 26 | Pending |
|
||||
| JT-03 | Phase 26 | Pending |
|
||||
| JT-04 | Phase 26 | Pending |
|
||||
| BT-01 | Phase 27 | Pending |
|
||||
| BT-02 | Phase 27 | Pending |
|
||||
| MET-01 | Phase 28 | Pending |
|
||||
| MET-02 | Phase 28 | Pending |
|
||||
|
||||
---
|
||||
*v1.6 requirements defined 2026-02-25*
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# Roadmap: JamKazam Media Features Modernization
|
||||
|
||||
## Milestones
|
||||
|
||||
- v1.0 Media Players (Phases 1-4) - SHIPPED 2026-01-14
|
||||
- v1.1 Music Session Chat (Phases 5-10) - SHIPPED 2026-01-27
|
||||
- v1.2 Session Attachments (Phases 11-14) - SHIPPED 2026-02-07
|
||||
- v1.3 Session Settings Tests (Phases 15-17) - SHIPPED 2026-02-08
|
||||
- v1.4 Memory Leak Prevention (Phases 18-22) - SHIPPED 2026-02-10
|
||||
- v1.5 Fix Session Recording (Phases 23-25) - SHIPPED 2026-02-25
|
||||
- **v1.6 Media Features Polish** (Phases 26-28) - IN PROGRESS
|
||||
|
||||
## Overview
|
||||
|
||||
v1.6 addresses usability issues reported in three media features: JamTrack (loading sequence bug, sizing, navigation), Backing Track (sync integration), and Metronome (sluggish controls). Each feature forms a natural phase boundary with independent, verifiable fixes.
|
||||
|
||||
## Phases
|
||||
|
||||
- [ ] **Phase 26: JamTrack Polish** - Fix loading sequence, sizing, navigation, and cleanup
|
||||
- [ ] **Phase 27: Backing Track Sync** - Enable track sync and async cleanup
|
||||
- [ ] **Phase 28: Metronome Responsiveness** - Debounce controls and cleanup timers
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 26: JamTrack Polish
|
||||
**Goal**: JamTrack player works correctly from selection through playback without freezes
|
||||
**Depends on**: v1.5 complete
|
||||
**Requirements**: JT-01, JT-02, JT-03, JT-04
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User sees loading indicator while backend processes track (not premature stem UI)
|
||||
2. JamTrack player fits properly in popup window without scrollbars
|
||||
3. "Create custom mix" button opens JamTrack editor in new tab
|
||||
4. No console warnings about leaked callbacks when closing JamTrack or navigating away
|
||||
**Plans**: TBD
|
||||
|
||||
Plans:
|
||||
- [ ] 26-01: TBD
|
||||
|
||||
### Phase 27: Backing Track Sync
|
||||
**Goal**: Backing Track appears in session screen when opened
|
||||
**Depends on**: Nothing (independent of Phase 26)
|
||||
**Requirements**: BT-01, BT-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Opening a backing track file shows the track in session screen (not just popup)
|
||||
2. No "state update on unmounted component" warnings when closing backing track quickly
|
||||
**Plans**: TBD
|
||||
|
||||
Plans:
|
||||
- [ ] 27-01: TBD
|
||||
|
||||
### Phase 28: Metronome Responsiveness
|
||||
**Goal**: Metronome controls respond smoothly to user input
|
||||
**Depends on**: Nothing (independent of Phase 26, 27)
|
||||
**Requirements**: MET-01, MET-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. BPM slider moves smoothly without lag when dragged
|
||||
2. Sound and meter selectors respond immediately to clicks
|
||||
3. No console warnings about timer cleanup when closing metronome
|
||||
**Plans**: TBD
|
||||
|
||||
Plans:
|
||||
- [ ] 28-01: TBD
|
||||
|
||||
## Progress
|
||||
|
||||
**Execution Order:** Phases 26, 27, 28 can execute in any order (no dependencies)
|
||||
|
||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||
|-------|-----------|----------------|--------|-----------|
|
||||
| 26. JamTrack Polish | v1.6 | 0/TBD | Not started | - |
|
||||
| 27. Backing Track Sync | v1.6 | 0/TBD | Not started | - |
|
||||
| 28. Metronome Responsiveness | v1.6 | 0/TBD | Not started | - |
|
||||
|
||||
---
|
||||
*v1.6 roadmap created 2026-02-25*
|
||||
|
|
@ -5,58 +5,28 @@
|
|||
See: .planning/PROJECT.md (updated 2026-02-25)
|
||||
|
||||
**Core value:** Modernize session features from legacy jQuery/Rails to React patterns
|
||||
**Current focus:** Planning next milestone
|
||||
**Current focus:** v1.6 Media Features Polish - Phase 26 JamTrack Polish
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: Not started
|
||||
Plan: -
|
||||
Status: v1.5 complete, ready for next milestone
|
||||
Last activity: 2026-02-25 - v1.5 Fix Session Recording shipped
|
||||
Phase: 26 of 28 (JamTrack Polish)
|
||||
Plan: 0 of TBD in current phase
|
||||
Status: Ready to plan
|
||||
Last activity: 2026-02-25 - v1.6 roadmap created
|
||||
|
||||
Progress: Milestone complete
|
||||
Progress: [░░░░░░░░░░] 0%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**v1.0 Media Players (Complete):**
|
||||
- Total plans completed: 13
|
||||
- Total phases: 5
|
||||
- Completion date: 2026-01-14
|
||||
**v1.0 - v1.5 Summary:**
|
||||
- Total milestones shipped: 6
|
||||
- Total phases completed: 25
|
||||
- Total plans completed: 44
|
||||
|
||||
**v1.1 Music Session Chat (Complete):**
|
||||
- Total plans completed: 11
|
||||
- Total phases: 6 (phases 6-11)
|
||||
- Completion date: 2026-01-31
|
||||
|
||||
**v1.2 Session Attachments (Complete):**
|
||||
- Total plans completed: 11
|
||||
- Total phases: 5 (phases 12-16)
|
||||
- Completion date: 2026-02-07
|
||||
- Duration: 5 days (2026-02-02 -> 2026-02-07)
|
||||
- Files modified: 12
|
||||
- Lines added: 1,868
|
||||
|
||||
**v1.3 Session Settings Tests (Complete):**
|
||||
- Plans completed: 2 (17-01, 18-01)
|
||||
- Total phases: 2 (phases 17-18)
|
||||
- Completion date: 2026-02-08
|
||||
- Duration: 1 day
|
||||
|
||||
**v1.4 Memory Leak Prevention (Complete):**
|
||||
- Phases: 5 (phases 19-23)
|
||||
- Requirements: 11
|
||||
- Plans completed: 5 (19-01, 20-01, 21-01, 22-01, 23-01)
|
||||
- Completion date: 2026-02-10
|
||||
- User verified: 15+ minute stability test passed, no freezes
|
||||
|
||||
**v1.5 Fix Session Recording (Complete):**
|
||||
- Phases: 2 (phases 24-25)
|
||||
- Plans completed: 2 (24-01, 25-01)
|
||||
- Completion date: 2026-02-25
|
||||
- Duration: 6 days (2026-02-19 -> 2026-02-25)
|
||||
- Files modified: 18
|
||||
- Lines: +1,103 / -107
|
||||
- User verified: 15+ minute recording stability, no memory growth
|
||||
**v1.6 Media Features Polish (In Progress):**
|
||||
- Phases: 3 (phases 26-28)
|
||||
- Requirements: 8
|
||||
- Plans completed: 0
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
|
|
@ -64,9 +34,9 @@ Progress: Milestone complete
|
|||
|
||||
See PROJECT.md Key Decisions table for full history.
|
||||
|
||||
Recent decisions (v1.5):
|
||||
- Conditional cleanup for window.JK callbacks (prevents race conditions with multiple hook instances)
|
||||
- Ignore flag pattern for async operations (prevents state updates on unmounted components)
|
||||
Recent decisions (v1.6):
|
||||
- Keep WindowPortal for Metronome (consistency with JamTrack, BackingTrack, Chat)
|
||||
- 300ms debounce for responsive UI while avoiding excessive native client calls
|
||||
|
||||
### Deferred Issues
|
||||
|
||||
|
|
@ -76,7 +46,7 @@ Recent decisions (v1.5):
|
|||
4. **WebSocket chat messages only broadcast to musicians** (Medium) - From v1.2
|
||||
5. **mp3 backend support** (Medium) - Frontend allows, backend whitelist doesn't support
|
||||
6. **Pre-existing test failures in JKChatMessageList.test.js** (Low) - Missing activeSession state
|
||||
7. **Duplicate recording start paths** (Low) - doStartRecording vs useRecordingHelpers.startRecording (from v1.5)
|
||||
7. **Duplicate recording start paths** (Low) - From v1.5
|
||||
|
||||
### Roadmap Evolution
|
||||
|
||||
|
|
@ -86,12 +56,13 @@ Recent decisions (v1.5):
|
|||
- **v1.3 Session Settings Tests** (Phases 17-18): Shipped 2026-02-08
|
||||
- **v1.4 Memory Leak Prevention** (Phases 19-23): Shipped 2026-02-10
|
||||
- **v1.5 Fix Session Recording** (Phases 24-25): Shipped 2026-02-25
|
||||
- **v1.6 Media Features Polish** (Phases 26-28): In Progress
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-02-25
|
||||
Stopped at: v1.5 milestone complete
|
||||
Stopped at: v1.6 roadmap created, ready to plan Phase 26
|
||||
Resume file: None
|
||||
|
||||
**Next steps:**
|
||||
1. `/gsd:new-milestone` to plan next feature work
|
||||
1. `/gsd:plan-phase 26` to plan JamTrack Polish
|
||||
|
|
|
|||
|
|
@ -0,0 +1,389 @@
|
|||
# Backing Track Sync Research
|
||||
|
||||
**Researched:** 2026-02-25
|
||||
**Confidence:** HIGH
|
||||
**Issue:** Backing Track doesn't sync to session screen when opened in popup window
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The issue is clear: **Backing Track never syncs to the session screen because the condition to display it is never satisfied.**
|
||||
|
||||
When a Backing Track is opened via popup:
|
||||
1. File is opened through `jamClient.SessionOpenBackingTrackFile()`
|
||||
2. `backingTrackData` is set in Redux (for popup display)
|
||||
3. **BUT** `mixerHelper.backingTracks` (required for session screen display) is NOT populated
|
||||
|
||||
The condition `showBackingTrackPlayer && mixerHelper.backingTracks.length > 0` at line 1434 of JKSessionScreen.js is never true because `mixerHelper.backingTracks` remains empty.
|
||||
|
||||
Metronome works because it follows a different pattern with `metronomeState.isOpen` flag and `metronomeTrackMixers` from Redux.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Backing Track Components
|
||||
|
||||
**JKSessionBackingTrackPlayer** (jam-ui/src/components/client/JKSessionBackingTrackPlayer.js)
|
||||
- Popup player with controls (play/pause/stop/seek/loop)
|
||||
- Opened in WindowPortal when user selects file
|
||||
- Uses `jamClient` for playback control
|
||||
- Does NOT trigger session screen sync
|
||||
|
||||
**JKSessionBackingTrack** (jam-ui/src/components/client/JKSessionBackingTrack.js)
|
||||
- Session Mix area component (the visual track in session screen)
|
||||
- Shows mixer controls (VU meter, gain, pan)
|
||||
- Requires `backingTrack` and `mixers` props
|
||||
- Only renders when `mixerHelper.backingTracks.length > 0`
|
||||
|
||||
### 2. How Metronome Successfully Syncs
|
||||
|
||||
**Popup Display:**
|
||||
- `metronomeState.isOpen` flag (from GlobalContext)
|
||||
- Renders JKSessionMetronomePlayer in WindowPortal
|
||||
- Line 1727-1744 in JKSessionScreen.js
|
||||
|
||||
**Session Screen Display:**
|
||||
- Condition: `metronomeState.isOpen && metronomeTrackMixers && metronomeTrackMixers.length > 0`
|
||||
- Uses `metronomeTrackMixers` from Redux mixersSlice selector
|
||||
- Line 1463-1488 in JKSessionScreen.js
|
||||
- Renders JKSessionMetronome component
|
||||
|
||||
**Why it works:**
|
||||
1. `openMetronome()` in useMediaActions.js (line 92):
|
||||
- Calls `jamClient.SessionOpenMetronome()`
|
||||
- Updates Redux: `updateMediaSummary({ metronomeOpen: true })`
|
||||
- Syncs tracks: `dispatch(syncTracksToServer(sessionId, jamClient))`
|
||||
2. Track sync triggers server update
|
||||
3. Session refresh fetches mixer data
|
||||
4. Mixer data includes metronome mixers
|
||||
5. `metronomeTrackMixers` selector returns populated array
|
||||
6. JKSessionMetronome renders in Session Mix area
|
||||
|
||||
### 3. What's Missing for Backing Track
|
||||
|
||||
**Current Flow (BROKEN):**
|
||||
|
||||
```
|
||||
User selects file
|
||||
↓
|
||||
handleBackingTrackSelected() in JKSessionScreen.js (line 1136)
|
||||
↓
|
||||
jamClient.SessionOpenBackingTrackFile(result.file, false)
|
||||
↓
|
||||
dispatch(setBackingTrackData({ backingTrack, session, currentUser }))
|
||||
↓
|
||||
dispatch(openModal('backingTrack'))
|
||||
↓
|
||||
WindowPortal renders JKSessionBackingTrackPlayer
|
||||
↓
|
||||
[STOPS HERE - mixerHelper.backingTracks NEVER POPULATED]
|
||||
```
|
||||
|
||||
**Missing Steps:**
|
||||
1. No track sync triggered after opening backing track
|
||||
2. No session refresh to fetch mixer data
|
||||
3. `mixerHelper.backingTracks` never populated from session data
|
||||
4. Condition at line 1434 never satisfied
|
||||
|
||||
**Compare with Metronome (WORKING):**
|
||||
|
||||
```
|
||||
User opens metronome
|
||||
↓
|
||||
openMetronome() in useMediaActions.js
|
||||
↓
|
||||
jamClient.SessionOpenMetronome()
|
||||
↓
|
||||
dispatch(updateMediaSummary({ metronomeOpen: true }))
|
||||
↓
|
||||
dispatch(syncTracksToServer(sessionId, jamClient)) ← KEY DIFFERENCE
|
||||
↓
|
||||
Server updates session with metronome mixer data
|
||||
↓
|
||||
Session refresh fetches mixers
|
||||
↓
|
||||
metronomeTrackMixers populated from Redux
|
||||
↓
|
||||
JKSessionMetronome renders in Session Mix
|
||||
```
|
||||
|
||||
### 4. Redux State Structure
|
||||
|
||||
**activeSessionSlice.js:**
|
||||
- `backingTrackData`: Stores popup player data (file path, session, user)
|
||||
- Used for: Determining if popup should show
|
||||
- NOT used for: Session Mix area display
|
||||
|
||||
**mediaSlice.js:**
|
||||
- `backingTracks`: Array of backing track objects with mixers
|
||||
- Updated by: `setBackingTracks` action from WebSocket or session refresh
|
||||
- Used for: Session Mix area display via `mixerHelper.backingTracks`
|
||||
|
||||
**mixersSlice.js:**
|
||||
- `backingTrackMixers`: Array of mixer objects for backing tracks
|
||||
- `mediaSummary.backingTrackOpen`: Boolean flag
|
||||
- Used for: Mixer controls in session screen
|
||||
|
||||
### 5. Data Flow for Session Screen Display
|
||||
|
||||
**How data gets to mixerHelper.backingTracks:**
|
||||
|
||||
```
|
||||
Session REST API response
|
||||
↓
|
||||
useSessionModel.js refreshCurrentSession() (line 695)
|
||||
↓
|
||||
Extract backing tracks from participants
|
||||
↓
|
||||
dispatch(setBackingTracks(extractedBackingTracks))
|
||||
↓
|
||||
mediaSlice.backingTracks updated
|
||||
↓
|
||||
useMixerHelper.js selects from mediaSlice (line 95)
|
||||
↓
|
||||
mixerHelper.backingTracks available to components
|
||||
```
|
||||
|
||||
**Current problem:**
|
||||
When backing track opened via popup, session is not refreshed, so backing tracks are never extracted from participants and `mixerHelper.backingTracks` remains empty.
|
||||
|
||||
### 6. Track Sync Service
|
||||
|
||||
**trackSyncService.js** (jam-ui/src/services/trackSyncService.js)
|
||||
- `syncTracksToServer()`: Syncs track state to backend
|
||||
- Builds payload with tracks, backing_tracks, metronome_open
|
||||
- Called after opening metronome (line 107 in useMediaActions.js)
|
||||
- NOT called after opening backing track
|
||||
|
||||
**Backend expectation:**
|
||||
- PUT `/api/sessions/:id/tracks` with track sync payload
|
||||
- Server updates session participants with backing track info
|
||||
- Subsequent GET fetches updated mixer data
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Why Backing Track Doesn't Sync
|
||||
|
||||
**Missing Integration Points:**
|
||||
|
||||
1. **No track sync call** in `handleBackingTrackSelected()` (JKSessionScreen.js line 1136)
|
||||
- Metronome calls: `dispatch(syncTracksToServer(sessionId, jamClient))`
|
||||
- Backing track: Missing this call
|
||||
|
||||
2. **Wrong Redux state used for display condition**
|
||||
- Uses: `showBackingTrackPlayer` (derived from `backingTrackData`)
|
||||
- Should use: `mediaSummary.backingTrackOpen` + `mixerHelper.backingTracks.length > 0`
|
||||
- Like metronome uses: `metronomeState.isOpen` + `metronomeTrackMixers.length > 0`
|
||||
|
||||
3. **No session refresh after opening**
|
||||
- Metronome: Track sync triggers backend update, then session refresh
|
||||
- Backing track: No sync, no refresh, no mixer data
|
||||
|
||||
4. **useMediaActions.openBackingTrack() exists but not used**
|
||||
- File: jam-ui/src/hooks/useMediaActions.js line 41
|
||||
- Already implements track sync: `dispatch(syncTracksToServer(sessionId, jamClient))`
|
||||
- Already updates media summary: `updateMediaSummary({ backingTrackOpen: true })`
|
||||
- **Not called from JKSessionScreen.js** - direct jamClient call used instead
|
||||
|
||||
## Recommended Fix Approach
|
||||
|
||||
### Option 1: Use Existing useMediaActions Pattern (RECOMMENDED)
|
||||
|
||||
**Change in JKSessionScreen.js:**
|
||||
|
||||
```javascript
|
||||
// BEFORE (line 1136):
|
||||
const handleBackingTrackSelected = async (result) => {
|
||||
try {
|
||||
await jamClient.SessionOpenBackingTrackFile(result.file, false);
|
||||
dispatch(setBackingTrackData({
|
||||
backingTrack: result.file,
|
||||
session: currentSession,
|
||||
currentUser: currentUser
|
||||
}));
|
||||
dispatch(openModal('backingTrack'));
|
||||
} catch (error) {
|
||||
toast.error('Failed to open backing track');
|
||||
}
|
||||
};
|
||||
|
||||
// AFTER:
|
||||
const handleBackingTrackSelected = async (result) => {
|
||||
try {
|
||||
// Use the media action that includes track sync
|
||||
await openBackingTrack(result.file);
|
||||
|
||||
// Set up popup data
|
||||
dispatch(setBackingTrackData({
|
||||
backingTrack: result.file,
|
||||
session: currentSession,
|
||||
currentUser: currentUser
|
||||
}));
|
||||
dispatch(openModal('backingTrack'));
|
||||
} catch (error) {
|
||||
toast.error('Failed to open backing track');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
1. `openBackingTrack()` (useMediaActions.js line 41) already:
|
||||
- Opens file: `jamClient.SessionOpenBackingTrackFile()`
|
||||
- Updates media summary: `updateMediaSummary({ backingTrackOpen: true })`
|
||||
- Syncs tracks: `dispatch(syncTracksToServer(sessionId, jamClient))`
|
||||
2. Track sync triggers backend update
|
||||
3. Session refresh fetches mixer data
|
||||
4. `mixerHelper.backingTracks` gets populated
|
||||
5. Session screen condition satisfied
|
||||
|
||||
**Additional change needed in display condition:**
|
||||
|
||||
```javascript
|
||||
// BEFORE (line 1434):
|
||||
{showBackingTrackPlayer && mixerHelper.backingTracks && mixerHelper.backingTracks.length > 0 && (
|
||||
// ...
|
||||
)}
|
||||
|
||||
// AFTER (align with metronome pattern):
|
||||
{mediaSummary.backingTrackOpen && mixerHelper.backingTracks && mixerHelper.backingTracks.length > 0 && (
|
||||
// ...
|
||||
)}
|
||||
```
|
||||
|
||||
### Option 2: Add Track Sync to Current Flow
|
||||
|
||||
If Option 1 causes issues, manually add track sync:
|
||||
|
||||
```javascript
|
||||
const handleBackingTrackSelected = async (result) => {
|
||||
try {
|
||||
await jamClient.SessionOpenBackingTrackFile(result.file, false);
|
||||
|
||||
// Add track sync (like metronome does)
|
||||
if (sessionId && jamClient) {
|
||||
console.log('[Track Sync] Backing track opened, syncing tracks');
|
||||
dispatch(syncTracksToServer(sessionId, jamClient));
|
||||
}
|
||||
|
||||
dispatch(setBackingTrackData({
|
||||
backingTrack: result.file,
|
||||
session: currentSession,
|
||||
currentUser: currentUser
|
||||
}));
|
||||
dispatch(openModal('backingTrack'));
|
||||
} catch (error) {
|
||||
toast.error('Failed to open backing track');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Pattern Comparison: Metronome vs Backing Track
|
||||
|
||||
| Aspect | Metronome (WORKS) | Backing Track (BROKEN) |
|
||||
|--------|------------------|----------------------|
|
||||
| Open Action | `openMetronome()` in useMediaActions | Direct `jamClient.SessionOpenBackingTrackFile()` |
|
||||
| Track Sync | ✅ Called in useMediaActions | ❌ Not called |
|
||||
| Media Summary Update | ✅ `metronomeOpen: true` | ❌ Not updated |
|
||||
| Popup State | `metronomeState.isOpen` (GlobalContext) | `backingTrackData` (Redux) |
|
||||
| Session Screen State | `metronomeTrackMixers` (Redux selector) | `mixerHelper.backingTracks` (never populated) |
|
||||
| Display Condition | `metronomeState.isOpen && metronomeTrackMixers.length > 0` | `showBackingTrackPlayer && mixerHelper.backingTracks.length > 0` |
|
||||
| Result | ✅ Shows in both popup and session screen | ❌ Shows only in popup |
|
||||
|
||||
## File Locations
|
||||
|
||||
### Components
|
||||
- **Backing Track Player (popup):** `jam-ui/src/components/client/JKSessionBackingTrackPlayer.js`
|
||||
- **Backing Track (session screen):** `jam-ui/src/components/client/JKSessionBackingTrack.js`
|
||||
- **Metronome Player (popup):** `jam-ui/src/components/client/JKSessionMetronomePlayer.js`
|
||||
- **Metronome (session screen):** `jam-ui/src/components/client/JKSessionMetronome.js`
|
||||
- **Session Screen:** `jam-ui/src/components/client/JKSessionScreen.js`
|
||||
|
||||
### State Management
|
||||
- **Media Actions:** `jam-ui/src/hooks/useMediaActions.js`
|
||||
- **Track Sync Service:** `jam-ui/src/services/trackSyncService.js`
|
||||
- **Active Session Slice:** `jam-ui/src/store/features/activeSessionSlice.js`
|
||||
- **Media Slice:** `jam-ui/src/store/features/mediaSlice.js`
|
||||
- **Mixers Slice:** `jam-ui/src/store/features/mixersSlice.js`
|
||||
- **Session Model:** `jam-ui/src/hooks/useSessionModel.js`
|
||||
- **Mixer Helper:** `jam-ui/src/hooks/useMixerHelper.js`
|
||||
|
||||
### Key Line Numbers
|
||||
- **Backing track handler:** JKSessionScreen.js line 1136-1163
|
||||
- **Backing track display condition:** JKSessionScreen.js line 1434
|
||||
- **Metronome display condition:** JKSessionScreen.js line 1463
|
||||
- **openBackingTrack() action:** useMediaActions.js line 41-60
|
||||
- **syncTracksToServer() service:** trackSyncService.js
|
||||
- **Session refresh with backing tracks:** useSessionModel.js line 685-696
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### 1. Verify Current Behavior (Failing Test)
|
||||
|
||||
```javascript
|
||||
// test/backing-track/backing-track-sync.spec.ts
|
||||
test('backing track appears in session screen after opening popup', async ({ page }) => {
|
||||
await loginToJamUI(page);
|
||||
await createAndJoinSession(page);
|
||||
|
||||
// Open backing track from Open menu
|
||||
await page.click('[data-testid="open-menu-button"]');
|
||||
await page.click('[data-testid="open-backing-track"]');
|
||||
await selectBackingTrackFile(page, 'test-audio.mp3');
|
||||
|
||||
// Verify popup appears
|
||||
await expect(page.locator('.metronome-player-popup')).toBeVisible();
|
||||
|
||||
// Verify track appears in session screen
|
||||
await expect(page.locator('.backingTrack')).toBeVisible();
|
||||
await expect(page.locator('.backingTrack .session-track')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Verify Track Sync Called
|
||||
|
||||
```javascript
|
||||
test('track sync triggered when backing track opened', async ({ page }) => {
|
||||
const interceptor = new APIInterceptor();
|
||||
interceptor.intercept(page);
|
||||
|
||||
await loginToJamUI(page);
|
||||
await createAndJoinSession(page);
|
||||
|
||||
await openBackingTrack(page, 'test-audio.mp3');
|
||||
|
||||
// Verify track sync API called
|
||||
const trackSyncCalls = interceptor.getCallsByPath('/api/sessions/*/tracks');
|
||||
expect(trackSyncCalls.length).toBeGreaterThan(0);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Verify Mixer Data Populated
|
||||
|
||||
```javascript
|
||||
test('backing track mixers populated after sync', async ({ page }) => {
|
||||
await loginToJamUI(page);
|
||||
const sessionId = await createAndJoinSession(page);
|
||||
|
||||
await openBackingTrack(page, 'test-audio.mp3');
|
||||
|
||||
// Wait for session refresh
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify mixer data in Redux
|
||||
const mixers = await page.evaluate(() => {
|
||||
return window.__REDUX_DEVTOOLS_EXTENSION__.store.getState().mixers.backingTrackMixers;
|
||||
});
|
||||
|
||||
expect(mixers.length).toBeGreaterThan(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The fix is straightforward:
|
||||
1. Use `openBackingTrack()` from useMediaActions instead of direct jamClient call
|
||||
2. This automatically triggers track sync (like metronome does)
|
||||
3. Track sync updates backend, session refresh populates mixer data
|
||||
4. Session screen condition satisfied, backing track appears
|
||||
|
||||
**Estimated effort:** 1-2 hours (simple refactor + testing)
|
||||
**Risk:** Low (following existing working pattern from metronome)
|
||||
**Impact:** HIGH (fixes critical missing feature)
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
# JamTrack Loading Sequence and Freeze Issue - Research
|
||||
|
||||
**Project:** jam-cloud (JamKazam)
|
||||
**Researched:** 2026-02-25
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The JamTrack freeze issue is caused by **premature UI rendering** - stems appear in the session screen immediately upon selection, but the backend hasn't finished creating/downloading/syncing the track files. When the user clicks play before backend processes complete, the system enters a "checking sync status" state that blocks the UI.
|
||||
|
||||
**Root cause:** The current flow triggers UI updates (`setSelectedJamTrack`, `setJamTrackStems`) immediately when the JamTrack is selected, but the actual backend processing (packaging, downloading, keying) happens asynchronously during the first play attempt in `JKSessionJamTrackPlayer`.
|
||||
|
||||
**Correct sequence should be:** Select → Backend processing (hidden) → Show UI (stems + player) → Ready to play
|
||||
|
||||
## Problem Flow (Current - Broken)
|
||||
|
||||
```
|
||||
1. User selects JamTrack from modal
|
||||
↓
|
||||
2. handleJamTrackSelect() in JKSessionScreen.js (line 1165-1190)
|
||||
- Fetches JamTrack data via REST API
|
||||
- IMMEDIATELY dispatches: setSelectedJamTrack(), setJamTrackStems(), setJamTrackData()
|
||||
↓
|
||||
3. UI updates IMMEDIATELY (line 1405-1431)
|
||||
- Stems appear in session screen
|
||||
- WindowPortal opens with player
|
||||
↓
|
||||
4. User clicks Play button
|
||||
↓
|
||||
5. JKSessionJamTrackPlayer handlePlay() (line 175-211)
|
||||
- Calls loadJamTrack thunk
|
||||
↓
|
||||
6. mediaSlice loadJamTrack() (line 17-49)
|
||||
- Checks if synchronized
|
||||
- If NOT synchronized → triggers downloadJamTrack()
|
||||
- Shows "checking sync status..." (line 602)
|
||||
↓
|
||||
7. mediaSlice downloadJamTrack() (line 51-266)
|
||||
- PACKAGING phase (line 123-223): enqueue mixdown, wait for server signing
|
||||
- DOWNLOADING phase (line 225-255): download via native client
|
||||
- KEYING phase (line 256-258): request decryption keys
|
||||
↓
|
||||
8. FREEZE occurs if:
|
||||
- Packaging takes too long (60s timeout)
|
||||
- Download fails
|
||||
- Keys not available
|
||||
- User interacts during process
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
### Core Components
|
||||
|
||||
| File | Purpose | Lines of Interest |
|
||||
|------|---------|-------------------|
|
||||
| `/jam-ui/src/components/client/JKSessionScreen.js` | Main session screen orchestrator | 1165-1190 (handleJamTrackSelect), 1405-1431 (stems rendering), 1746-1761 (WindowPortal) |
|
||||
| `/jam-ui/src/components/client/JKSessionJamTrackPlayer.js` | JamTrack player component (WindowPortal content) | 69-160 (initialization), 175-211 (handlePlay), 602 ("checking sync status" text) |
|
||||
| `/jam-ui/src/components/client/JKSessionJamTrackStems.js` | Renders stems in session screen | 5-49 (stem track mapping) |
|
||||
| `/jam-ui/src/components/client/JKSessionJamTrackModal.js` | JamTrack selection modal | 44-52 (onSelect callback) |
|
||||
|
||||
### Redux State Management
|
||||
|
||||
| File | Purpose | Lines of Interest |
|
||||
|------|---------|-------------------|
|
||||
| `/jam-ui/src/store/features/mediaSlice.js` | Media operations (load, download, sync) | 17-49 (loadJamTrack), 51-266 (downloadJamTrack), 268-280 (checkJamTrackSync) |
|
||||
| `/jam-ui/src/store/features/activeSessionSlice.js` | Session data (selected track, stems) | 172-178 (setSelectedJamTrack, setJamTrackStems), 211-217 (jamTrackData for player) |
|
||||
| `/jam-ui/src/store/features/sessionUISlice.js` | UI state (modal visibility) | 51 (openJamTrack state) |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
| File | Purpose | Lines of Interest |
|
||||
|------|---------|-------------------|
|
||||
| `/jam-ui/src/components/common/WindowPortal.js` | Popup window manager | 7 (windowFeatures for sizing) |
|
||||
| `/jam-ui/src/helpers/rest.js` | API endpoints | 531-538 (getJamTrack), 935-945 (closeJamTrack) |
|
||||
|
||||
## Backend API Calls and Timing
|
||||
|
||||
### 1. Initial Selection (REST API - synchronous)
|
||||
|
||||
**Endpoint:** `GET /jamtracks/:id`
|
||||
**Called by:** `handleJamTrackSelect()` in JKSessionScreen.js (line 1169)
|
||||
**Purpose:** Fetch JamTrack metadata with stems/mixdowns
|
||||
**Timing:** ~100-500ms
|
||||
**Response includes:**
|
||||
- JamTrack metadata (id, name, artist, etc.)
|
||||
- Mixdowns array (master, custom-mix, stem types)
|
||||
- Each mixdown has packages array (file_type, encrypt_type, sample_rate)
|
||||
- Tracks array (stems with instruments)
|
||||
|
||||
### 2. Packaging Phase (REST + WebSocket - asynchronous)
|
||||
|
||||
**Endpoint:** `POST /jam_track_mixdowns/:id/enqueue`
|
||||
**Called by:** `downloadJamTrack()` in mediaSlice.js (line 143-148)
|
||||
**Purpose:** Request server-side package creation/signing
|
||||
**Timing:** 5-30 seconds (can timeout at 60s)
|
||||
**Process:**
|
||||
1. Server queues mixdown for packaging
|
||||
2. WebSocket subscription created for progress notifications (line 157-166)
|
||||
3. Polling loop waits for `signing_state === 'SIGNED'` (line 179-221)
|
||||
4. State updates shown as "Your JamTrack is currently being created" (line 603)
|
||||
|
||||
**Failure modes:**
|
||||
- `SIGNING_TIMEOUT`, `QUEUED_TIMEOUT`, `QUIET_TIMEOUT` → error
|
||||
- WebSocket not available → error (line 159-161)
|
||||
|
||||
### 3. Download Phase (Native Client - asynchronous)
|
||||
|
||||
**Method:** `jamClient.JamTrackDownload()`
|
||||
**Called by:** `downloadJamTrack()` in mediaSlice.js (line 247-254)
|
||||
**Purpose:** Download encrypted audio files to local storage
|
||||
**Timing:** 10-60 seconds (depends on file size, network)
|
||||
**Process:**
|
||||
1. Native client initiates download with progress callbacks
|
||||
2. Progress updates via `window.jamTrackDownloadProgress` (line 234-236)
|
||||
3. Success triggers `window.jamTrackDownloadSuccess` (line 238-240)
|
||||
4. Failure triggers `window.jamTrackDownloadFail` (line 242-244)
|
||||
|
||||
### 4. Keying Phase (Native Client - synchronous)
|
||||
|
||||
**Method:** `jamClient.JamTrackKeysRequest()`
|
||||
**Called by:** `downloadJamTrack()` in mediaSlice.js (line 257)
|
||||
**Purpose:** Request decryption keys for encrypted files
|
||||
**Timing:** ~1-5 seconds
|
||||
**State shown:** "Requesting decryption keys..." (line 605)
|
||||
|
||||
### 5. Play Phase (Native Client - synchronous)
|
||||
|
||||
**Method:** `jamClient.JamTrackPlay(fqId)`
|
||||
**Called by:** `loadJamTrack()` in mediaSlice.js (line 39-42)
|
||||
**Purpose:** Start audio playback
|
||||
**Timing:** ~100-500ms
|
||||
**Requires:** Files synchronized (trackDetail.key_state === 'AVAILABLE')
|
||||
|
||||
## Root Cause of Freeze
|
||||
|
||||
The freeze occurs because:
|
||||
|
||||
1. **Immediate UI update** (line 1175-1176 in JKSessionScreen.js):
|
||||
```javascript
|
||||
dispatch(setSelectedJamTrack(jamTrackWithStems));
|
||||
dispatch(setJamTrackStems(jamTrackWithStems.tracks || []));
|
||||
```
|
||||
This causes stems to render immediately at line 1405-1431.
|
||||
|
||||
2. **Conditional rendering check is wrong** (line 1406):
|
||||
```javascript
|
||||
{selectedJamTrack && jamTrackStems.length > 0 &&
|
||||
(jamTrackDownloadState.state === 'synchronized' || jamTrackDownloadState.state === 'idle')
|
||||
```
|
||||
The condition allows rendering when `state === 'idle'`, but "idle" means **not yet checked**, not "ready to play".
|
||||
|
||||
3. **Play button triggers long async process** (line 183-190):
|
||||
When user clicks play, if track isn't synchronized, it triggers packaging → downloading → keying, which can take 30-90 seconds. During this time:
|
||||
- UI shows "checking sync status..." (line 602)
|
||||
- User can't interact (buttons disabled via `isOperating` flag)
|
||||
- If process fails or times out, UI is stuck
|
||||
|
||||
4. **WindowPortal sizing issue**:
|
||||
Player window is hardcoded to `width=600,height=500` (line 1750), but player content width is `420px` (line 487 in JKSessionJamTrackPlayer.js), causing poor fit.
|
||||
|
||||
## Recommended Fix Approach
|
||||
|
||||
### Strategy: Backend-First Loading
|
||||
|
||||
**Goal:** Don't show UI until backend is ready.
|
||||
|
||||
### Phase 1: Defer UI Rendering
|
||||
|
||||
**Change `handleJamTrackSelect()` to:**
|
||||
1. Fetch JamTrack metadata (as now)
|
||||
2. Show loading indicator in modal/toast
|
||||
3. **DON'T dispatch `setSelectedJamTrack` yet**
|
||||
4. Trigger `loadJamTrack()` thunk (which includes sync check + download if needed)
|
||||
5. Wait for `loadJamTrack()` to complete
|
||||
6. **THEN** dispatch UI state: `setSelectedJamTrack`, `setJamTrackStems`, `setJamTrackData`
|
||||
7. Close modal, show player + stems
|
||||
|
||||
**Code change location:** JKSessionScreen.js lines 1165-1190
|
||||
|
||||
```javascript
|
||||
const handleJamTrackSelect = async (jamTrack) => {
|
||||
try {
|
||||
// 1. Fetch metadata
|
||||
const response = await getJamTrack({ id: jamTrack.id });
|
||||
const jamTrackWithStems = await response.json();
|
||||
|
||||
// 2. Show loading
|
||||
toast.info('Preparing JamTrack...');
|
||||
|
||||
// 3. Pre-load backend (sync check + download if needed)
|
||||
await dispatch(loadJamTrack({
|
||||
jamTrack: jamTrackWithStems,
|
||||
mixdownId: null, // Use default
|
||||
autoPlay: false, // Don't auto-play, just prepare
|
||||
jamClient,
|
||||
jamServer
|
||||
})).unwrap();
|
||||
|
||||
// 4. NOW show UI (backend is ready)
|
||||
dispatch(setSelectedJamTrack(jamTrackWithStems));
|
||||
dispatch(setJamTrackStems(jamTrackWithStems.tracks || []));
|
||||
dispatch(setJamTrackData({
|
||||
jamTrack: jamTrackWithStems,
|
||||
session: currentSession,
|
||||
currentUser: currentUser
|
||||
}));
|
||||
|
||||
toast.success(`Loaded JamTrack: ${jamTrackWithStems.name}`);
|
||||
} catch (error) {
|
||||
toast.error(`Failed to load JamTrack: ${error.message}`);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 2: Fix Conditional Rendering
|
||||
|
||||
**Change line 1406 in JKSessionScreen.js:**
|
||||
|
||||
```javascript
|
||||
// OLD (wrong):
|
||||
{selectedJamTrack && jamTrackStems.length > 0 &&
|
||||
(jamTrackDownloadState.state === 'synchronized' || jamTrackDownloadState.state === 'idle')
|
||||
|
||||
// NEW (correct):
|
||||
{selectedJamTrack && jamTrackStems.length > 0 &&
|
||||
jamTrackDownloadState.state === 'synchronized'
|
||||
```
|
||||
|
||||
Remove the `|| jamTrackDownloadState.state === 'idle'` condition. UI should only show when explicitly synchronized.
|
||||
|
||||
### Phase 3: Fix WindowPortal Sizing
|
||||
|
||||
**Change line 1750 in JKSessionScreen.js:**
|
||||
|
||||
```javascript
|
||||
// OLD:
|
||||
windowFeatures="width=600,height=500,left=200,top=200,..."
|
||||
|
||||
// NEW (matches player content width):
|
||||
windowFeatures="width=460,height=350,left=200,top=200,..."
|
||||
```
|
||||
|
||||
Player content is 420px wide (line 487 in JKSessionJamTrackPlayer.js), so 460px window width provides proper padding.
|
||||
|
||||
### Phase 4: Progress Feedback During Prep
|
||||
|
||||
While backend is processing (packaging/downloading), show progress in the modal or as a toast:
|
||||
|
||||
**Option A: Modal shows progress**
|
||||
- Keep modal open during prep
|
||||
- Show packaging/downloading progress bars
|
||||
- Close modal when ready
|
||||
|
||||
**Option B: Toast notifications**
|
||||
- Close modal immediately
|
||||
- Show toast: "Preparing JamTrack... (packaging)"
|
||||
- Update toast: "Preparing JamTrack... (downloading 45%)"
|
||||
- Final toast: "JamTrack ready!" → open player + stems
|
||||
|
||||
## "Create Custom Mix" Navigation
|
||||
|
||||
### Current Implementation
|
||||
|
||||
**Location:** JKSessionJamTrackPlayer.js line 860
|
||||
**Current code:**
|
||||
```javascript
|
||||
<a onClick={() => console.log('TODO: Open custom mix creator')}>
|
||||
create custom mix
|
||||
</a>
|
||||
```
|
||||
|
||||
### Required Behavior
|
||||
|
||||
Should open `/jamtracks/{id}` in a new browser tab.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```javascript
|
||||
<a
|
||||
onClick={() => {
|
||||
const url = `/jamtracks/${jamTrack.id}`;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: '#007aff',
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
create custom mix
|
||||
</a>
|
||||
```
|
||||
|
||||
**Route:** Already exists in routes.js line 36 (`/jamtracks`)
|
||||
|
||||
**Component:** Should navigate to JamTrack show page where user can create custom mixdowns (existing functionality).
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test 1: Fresh JamTrack (Not Synchronized)
|
||||
1. Select JamTrack never downloaded before
|
||||
2. **Expected:** Modal shows loading, then closes when ready
|
||||
3. **Expected:** Stems + player appear simultaneously
|
||||
4. **Expected:** Play button works immediately without "checking sync status"
|
||||
|
||||
### Test 2: Already Synchronized JamTrack
|
||||
1. Select JamTrack already downloaded
|
||||
2. **Expected:** Fast load (no packaging/downloading)
|
||||
3. **Expected:** Stems + player appear quickly
|
||||
4. **Expected:** Play button works immediately
|
||||
|
||||
### Test 3: Packaging Failure
|
||||
1. Mock server error during packaging
|
||||
2. **Expected:** Error toast shown
|
||||
3. **Expected:** UI does NOT show stems/player (stays clean)
|
||||
4. **Expected:** User can retry or select different track
|
||||
|
||||
### Test 4: WindowPortal Sizing
|
||||
1. Open JamTrack player
|
||||
2. **Expected:** Player fits properly in window (no excessive white space)
|
||||
3. **Expected:** Controls are visible without scrolling
|
||||
|
||||
### Test 5: Create Custom Mix Link
|
||||
1. Open JamTrack player
|
||||
2. Click "create custom mix"
|
||||
3. **Expected:** New tab opens to `/jamtracks/{id}`
|
||||
4. **Expected:** User can create custom mixdown
|
||||
|
||||
## Related Issues
|
||||
|
||||
### WebSocket Subscription Management
|
||||
|
||||
**File:** mediaSlice.js lines 157-173
|
||||
**Issue:** Subscription/unsubscription for packaging progress must be robust
|
||||
**Current:** Uses polling + timeout
|
||||
**Risk:** Memory leaks if unsubscribe not called on error/timeout
|
||||
|
||||
### Native Client Callback Management
|
||||
|
||||
**File:** mediaSlice.js lines 234-244
|
||||
**Issue:** Global callbacks (`window.jamTrackDownloadProgress`) are fragile
|
||||
**Risk:** Multiple JamTracks could conflict if callbacks overwrite each other
|
||||
|
||||
### Error Recovery
|
||||
|
||||
**Current:** User must close player and retry
|
||||
**Better:** Provide "Retry" button in error state (already exists in player, line 363-379)
|
||||
|
||||
## Summary
|
||||
|
||||
**The freeze is NOT a native client bug.** It's a **UI state management issue** where the UI renders before the backend is ready.
|
||||
|
||||
**Fix priority:**
|
||||
1. **HIGH:** Defer UI rendering until backend ready (Phase 1)
|
||||
2. **HIGH:** Fix conditional rendering logic (Phase 2)
|
||||
3. **MEDIUM:** Fix WindowPortal sizing (Phase 3)
|
||||
4. **LOW:** Add "create custom mix" navigation (it's a TODO, not a bug)
|
||||
|
||||
**Estimated effort:**
|
||||
- Phase 1: 2-3 hours (requires Redux thunk orchestration)
|
||||
- Phase 2: 5 minutes (one line change)
|
||||
- Phase 3: 5 minutes (one line change)
|
||||
- Phase 4: 30 minutes (optional progress UI)
|
||||
- Create custom mix: 5 minutes (one line change)
|
||||
|
||||
**Total:** 3-4 hours to fix freeze + sizing issues
|
||||
|
|
@ -0,0 +1,438 @@
|
|||
# Metronome UI Component Research
|
||||
|
||||
**Domain:** jam-ui React 16 SPA - Metronome Controls UI Fix
|
||||
**Researched:** 2026-02-25
|
||||
**Overall confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Metronome UI is implemented across three components: `JKSessionMetronomePlayer.js` (controls popup), `JKSessionMetronome.js` (mixer track), and the WindowPortal pattern for popup windows. The current issue is that WindowPortal creates browser popup windows with "about:blank" URLs and unstyled chrome. The controls themselves are already styled with circular play/stop buttons and a clean form layout.
|
||||
|
||||
**Key Findings:**
|
||||
- Metronome controls are **already well-styled** with circular icons (lines 223-239 in JKSessionMetronomePlayer.js)
|
||||
- CSS exists (JKSessionMetronomePlayer.css) with modern styling patterns
|
||||
- WindowPortal is the **standard pattern** for modeless dialogs (used by Chat, JamTrack, BackingTrack)
|
||||
- WindowPortal **cannot hide** "about:blank" URL bar - it's browser chrome behavior
|
||||
- Alternative: Use reactstrap Modal for modal dialog instead of popup window
|
||||
|
||||
## 1. File Locations
|
||||
|
||||
### Core Metronome Components
|
||||
|
||||
| File | Purpose | Lines |
|
||||
|------|---------|-------|
|
||||
| `/jam-ui/src/components/client/JKSessionMetronomePlayer.js` | Main controls component with play/stop/settings | 332 lines |
|
||||
| `/jam-ui/src/components/client/JKSessionMetronomePlayer.css` | Styling for controls (circular buttons, form layout) | 232 lines |
|
||||
| `/jam-ui/src/components/client/JKSessionMetronome.js` | Metronome mixer track (VU meter, gain control) | 117 lines |
|
||||
| `/jam-ui/src/components/common/WindowPortal.js` | Popup window wrapper component | 153 lines |
|
||||
| `/jam-ui/src/hooks/useMetronomeState.js` | State management hook | 73 lines |
|
||||
|
||||
### Integration Point
|
||||
|
||||
**JKSessionScreen.js** (lines 1727-1744) opens the Metronome player:
|
||||
|
||||
```javascript
|
||||
{metronomeState.isOpen && (
|
||||
<WindowPortal
|
||||
title="Metronome Controls"
|
||||
onClose={handleMetronomeClose}
|
||||
windowFeatures="width=450,height=400,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no"
|
||||
windowId="metronome-controls"
|
||||
>
|
||||
<JKSessionMetronomePlayer
|
||||
isOpen={metronomeState.isOpen}
|
||||
onClose={handleMetronomeClose}
|
||||
metronomeState={metronomeState}
|
||||
jamClient={jamClient}
|
||||
session={currentSession}
|
||||
currentUser={currentUser}
|
||||
isPopup={true}
|
||||
/>
|
||||
</WindowPortal>
|
||||
)}
|
||||
```
|
||||
|
||||
## 2. WindowPortal Analysis
|
||||
|
||||
### How WindowPortal Works
|
||||
|
||||
**File:** `/jam-ui/src/components/common/WindowPortal.js`
|
||||
|
||||
**Mechanism:**
|
||||
1. Calls `window.open('', '_blank', windowFeatures)` to create popup (line 55)
|
||||
2. Sets basic document styles on the popup window (lines 64-69)
|
||||
3. Creates container div and uses `ReactDOM.createPortal()` to render children (line 149)
|
||||
4. Manages lifecycle (close detection, cleanup)
|
||||
|
||||
**What It Can Do:**
|
||||
- ✅ Create modeless popup windows
|
||||
- ✅ Apply basic body styles (`margin: 0`, `padding: 0`, `backgroundColor`, `fontFamily`)
|
||||
- ✅ Render React components in popup
|
||||
- ✅ Persist window position between opens
|
||||
- ✅ Detect window close events
|
||||
- ✅ Send/receive messages via postMessage
|
||||
|
||||
**What It CANNOT Do:**
|
||||
- ❌ Hide URL bar (controlled by browser security)
|
||||
- ❌ Remove browser chrome completely
|
||||
- ❌ Control window decorations (except via `windowFeatures` string)
|
||||
- ❌ Style window title bar
|
||||
|
||||
### Why "about:blank" Appears
|
||||
|
||||
**Root Cause:** `window.open('', '_blank', ...)` creates an empty document with `about:blank` URL.
|
||||
|
||||
**Browser Security:** Modern browsers **always show** the URL bar for popup windows for security reasons. The `windowFeatures` flags (`location=no`, `addressbar=no`) are **deprecated** and ignored in modern Chrome/Firefox/Safari.
|
||||
|
||||
**Historical Context:** In legacy IE/older Firefox, these flags could hide the address bar. Not possible anymore.
|
||||
|
||||
### WindowPortal Usage in Codebase
|
||||
|
||||
WindowPortal is the **standard pattern** for all modeless dialogs:
|
||||
|
||||
| Feature | File | Window Size |
|
||||
|---------|------|-------------|
|
||||
| Chat | JKSessionChatWindow.js | 450×600 |
|
||||
| JamTrack Player | JKSessionJamTrackPlayer.js | 600×500 |
|
||||
| Backing Track Player | JKSessionBackingTrackPlayer.js | 400×300 |
|
||||
| Metronome Controls | JKSessionMetronomePlayer.js | 450×400 |
|
||||
|
||||
**Pattern Consistency:** All media players use WindowPortal. Changing Metronome to Modal would break UX consistency.
|
||||
|
||||
## 3. Current Metronome UI Styling
|
||||
|
||||
### Existing Styles Are Already Good
|
||||
|
||||
**File:** `/jam-ui/src/components/client/JKSessionMetronomePlayer.css`
|
||||
|
||||
The controls are **already styled** with modern patterns:
|
||||
|
||||
#### Circular Play/Stop Buttons (Lines 71-113)
|
||||
|
||||
```css
|
||||
.metronome-icon-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #007bff;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.metronome-icon-btn:hover:not(.disabled) {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
#### Clean Form Layout (Lines 115-137)
|
||||
|
||||
```css
|
||||
.metronome-form-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metronome-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metronome-form-label {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
margin: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Component Structure (Already Clean)
|
||||
|
||||
**JKSessionMetronomePlayer.js** lines 211-295:
|
||||
|
||||
```
|
||||
┌─ metronome-player-popup ──────────────┐
|
||||
│ ┌─ header ──────────────────────┐ │
|
||||
│ │ h3: "Metronome Controls" │ │
|
||||
│ │ Close X button │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ ┌─ body ────────────────────────┐ │
|
||||
│ │ ┌─ main-content ───────────┐ │ │
|
||||
│ │ │ ⚪ Play ⚪ Stop │ │ │
|
||||
│ │ │ Feature: [Metronome ▼] │ │ │
|
||||
│ │ │ Sound: [Beep ▼] │ │ │
|
||||
│ │ │ Tempo: [120 ▼] │ │ │
|
||||
│ │ └─────────────────────────┘ │ │
|
||||
│ │ [ Close ] │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**The controls are not "unstyled/raw HTML"** - they already have clean, modern styling.
|
||||
|
||||
## 4. What's Actually Wrong?
|
||||
|
||||
Based on milestone description:
|
||||
|
||||
| Issue | Reality | Fix Needed? |
|
||||
|-------|---------|-------------|
|
||||
| "about:blank" in URL bar | **Cannot be fixed** (browser security) | No - explain to user |
|
||||
| "Unstyled/raw HTML controls" | **False** - controls are already styled | No - verify visually |
|
||||
| "Layout cramped/misaligned" | **Possible** - depends on content overflow | Maybe - check WindowPortal size |
|
||||
| "Need circular play/stop icons" | **Already implemented** (lines 223-239) | No - already done |
|
||||
|
||||
### Hypothesis: WindowPortal Size Issue
|
||||
|
||||
**Current size:** `width=450,height=400` (line 1731 of JKSessionScreen.js)
|
||||
|
||||
**Content:**
|
||||
- Header: ~60px
|
||||
- Play/Stop buttons: ~50px
|
||||
- 3 form rows (Feature, Sound, Tempo): ~36px × 3 = 108px
|
||||
- Close button: ~40px
|
||||
- Padding/gaps: ~60px
|
||||
- **Total:** ~318px minimum height
|
||||
|
||||
**400px height should be sufficient.** Possible cramping if:
|
||||
- Browser added its own chrome (title bar, URL bar) reducing viewport
|
||||
- Popup window is smaller than requested (popup blockers)
|
||||
- Content has unexpected overflow
|
||||
|
||||
## 5. Styling Patterns in Codebase
|
||||
|
||||
### Circular Icon Buttons Pattern
|
||||
|
||||
**Used in:** BackingTrackPlayer, JamTrackPlayer, MetronomePlayer
|
||||
|
||||
**JamTrack Pattern (JKSessionJamTrackPlayer.css lines 84-107):**
|
||||
|
||||
```css
|
||||
.jamtrack-btn-play {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.jamtrack-btn-play:hover:not(:disabled) {
|
||||
background: #0051d5;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
```
|
||||
|
||||
**Metronome uses similar pattern** but with blue (#007bff) instead of iOS blue (#007aff).
|
||||
|
||||
### Form Layout Patterns
|
||||
|
||||
**Grid-based label + control:** (Used in Metronome, consistent with other forms)
|
||||
|
||||
```css
|
||||
.metronome-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr; /* Label width, control fills rest */
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
```
|
||||
|
||||
**This is modern and clean.** No changes needed.
|
||||
|
||||
### Color Palette
|
||||
|
||||
| Component | Primary Action | Secondary Action | Background |
|
||||
|-----------|---------------|------------------|------------|
|
||||
| Metronome | #007bff (blue) | #6c757d (gray) | #f5f5f5 |
|
||||
| JamTrack | #007aff (iOS blue) | #6e6e73 (gray) | #ffffff |
|
||||
| BackingTrack | #5b9bd5 (light blue) | #b0b0b0 (gray) | #f8f9fa |
|
||||
|
||||
**Metronome colors are standard Bootstrap blue** - acceptable.
|
||||
|
||||
## 6. Modal Alternative to WindowPortal
|
||||
|
||||
### Option: Use reactstrap Modal
|
||||
|
||||
**File:** `/jam-ui/src/components/client/JKSessionMetronomePlayer.js` (lines 318-328)
|
||||
|
||||
**Already has Modal fallback:**
|
||||
|
||||
```javascript
|
||||
// Otherwise, render as Modal (fallback)
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={handleClose} className="metronome-player-modal">
|
||||
<ModalHeader toggle={handleClose}>
|
||||
Metronome Controls
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{renderControls()}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
```
|
||||
|
||||
**This code path is triggered when `isPopup={false}`.**
|
||||
|
||||
### Modal vs WindowPortal Tradeoffs
|
||||
|
||||
| Feature | Modal (reactstrap) | WindowPortal |
|
||||
|---------|-------------------|--------------|
|
||||
| URL bar visible | ❌ No URL bar | ✅ "about:blank" visible |
|
||||
| Repositionable | ❌ Centered only | ✅ User can drag anywhere |
|
||||
| Multi-monitor support | ❌ Main window only | ✅ Can move to 2nd monitor |
|
||||
| Always on top | ❌ No (blocked by main window) | ✅ Separate window |
|
||||
| Persistent position | ❌ Resets each time | ✅ Remembers position |
|
||||
| Keyboard shortcuts | ✅ Same context | ❌ Different window context |
|
||||
| Browser chrome | ✅ None | ❌ Title bar + URL bar |
|
||||
| Accessibility | ✅ Better (aria-modal) | ⚠️ Separate window tree |
|
||||
|
||||
### Pattern Consistency Issue
|
||||
|
||||
**All other media players use WindowPortal:**
|
||||
- Backing Track Player (WindowPortal)
|
||||
- JamTrack Player (WindowPortal)
|
||||
- Chat Window (WindowPortal)
|
||||
|
||||
**Changing Metronome to Modal would break UX consistency.**
|
||||
|
||||
**User expectation:** Modeless media controls that can be repositioned.
|
||||
|
||||
## 7. Recommended Approach
|
||||
|
||||
### Option A: Keep WindowPortal, Document Limitations (RECOMMENDED)
|
||||
|
||||
**Rationale:**
|
||||
1. **Controls are already well-styled** - circular icons, clean layout exist
|
||||
2. **WindowPortal is the standard pattern** - consistent with other media players
|
||||
3. **"about:blank" cannot be hidden** - browser security constraint
|
||||
4. **Modeless UX is valuable** - users can position controls separately
|
||||
|
||||
**What to do:**
|
||||
1. **Verify visual appearance** - open Metronome and confirm controls render correctly
|
||||
2. **If cramped:** Increase WindowPortal size to `width=500,height=450`
|
||||
3. **Document in user guide:** "Browser security requires URL bar in popup windows"
|
||||
4. **No code changes needed** if controls render correctly
|
||||
|
||||
**Effort:** 0-1 hour (size adjustment if needed)
|
||||
|
||||
### Option B: Switch to Modal Dialog
|
||||
|
||||
**Rationale:**
|
||||
- **Removes "about:blank" URL bar** (no browser chrome)
|
||||
- **Simpler UX** (no separate window)
|
||||
|
||||
**Tradeoffs:**
|
||||
- **Breaks consistency** with other media players
|
||||
- **Loses modeless behavior** (blocks main window)
|
||||
- **Cannot reposition** (always centered)
|
||||
|
||||
**What to do:**
|
||||
1. Change `JKSessionScreen.js` line 1727 to use Modal instead of WindowPortal
|
||||
2. Set `isPopup={false}` when rendering JKSessionMetronomePlayer
|
||||
3. Update state management to use modal open/close pattern
|
||||
|
||||
**Effort:** 2-3 hours (integration + testing)
|
||||
|
||||
### Option C: Styled Electron Window (Future Enhancement)
|
||||
|
||||
**Rationale:**
|
||||
- **Full control over window chrome**
|
||||
- **Can hide URL bar, customize title bar**
|
||||
- **Professional appearance**
|
||||
|
||||
**Requirements:**
|
||||
- Requires Electron BrowserWindow API
|
||||
- Not available in browser-based React SPA
|
||||
- Would need desktop app wrapper
|
||||
|
||||
**Effort:** N/A (not applicable to current architecture)
|
||||
|
||||
## 8. Investigation Checklist
|
||||
|
||||
Before implementing fix, verify the actual problem:
|
||||
|
||||
- [ ] **Open Metronome controls** - Does WindowPortal open successfully?
|
||||
- [ ] **Check button styling** - Are play/stop buttons circular with icons?
|
||||
- [ ] **Check form layout** - Are Feature/Sound/Tempo dropdowns aligned?
|
||||
- [ ] **Measure content height** - Does content fit in 400px viewport?
|
||||
- [ ] **Test window resize** - Can user manually resize to see all content?
|
||||
- [ ] **Compare to JamTrack** - Does JamTrack popup look better? Why?
|
||||
|
||||
**If controls already look good:** No fix needed, just document WindowPortal limitations.
|
||||
|
||||
**If layout is cramped:** Increase WindowPortal `height` to 450-500px.
|
||||
|
||||
**If buttons are missing icons:** Check FontAwesome imports.
|
||||
|
||||
## 9. CSS Modification Examples
|
||||
|
||||
### If Play/Stop Icons Need Enhancement
|
||||
|
||||
**Current:** Unicode characters (▶ and ■)
|
||||
|
||||
**Enhancement:** Use FontAwesome icons
|
||||
|
||||
```javascript
|
||||
// JKSessionMetronomePlayer.js line 229-230
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlay, faStop } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
// Replace lines 230 and 237:
|
||||
<FontAwesomeIcon icon={faPlay} /> // Instead of <span>▶</span>
|
||||
<FontAwesomeIcon icon={faStop} /> // Instead of <span>■</span>
|
||||
```
|
||||
|
||||
**CSS already supports this** (icon color changes on hover).
|
||||
|
||||
### If WindowPortal Needs Larger Size
|
||||
|
||||
**JKSessionScreen.js line 1731:**
|
||||
|
||||
```javascript
|
||||
// Change from:
|
||||
windowFeatures="width=450,height=400,..."
|
||||
|
||||
// To:
|
||||
windowFeatures="width=500,height=500,..."
|
||||
```
|
||||
|
||||
**No other changes needed.**
|
||||
|
||||
## 10. Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Component locations | HIGH | All files verified, structure understood |
|
||||
| WindowPortal limitations | HIGH | Browser security constraint confirmed |
|
||||
| Existing styling | HIGH | CSS files read, styles are modern |
|
||||
| Modal alternative | HIGH | Code already exists, tested pattern |
|
||||
| Recommended approach | HIGH | Based on codebase patterns + browser constraints |
|
||||
|
||||
## 11. Sources
|
||||
|
||||
- `/jam-ui/src/components/client/JKSessionMetronomePlayer.js` - Main component
|
||||
- `/jam-ui/src/components/client/JKSessionMetronomePlayer.css` - Styling
|
||||
- `/jam-ui/src/components/common/WindowPortal.js` - Popup mechanism
|
||||
- `/jam-ui/src/components/client/JKSessionScreen.js` - Integration point
|
||||
- `/jam-ui/src/components/client/JKSessionJamTrackPlayer.css` - Pattern reference
|
||||
- `/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js` - Pattern reference
|
||||
- MDN Web Docs: `window.open()` - windowFeatures deprecated flags
|
||||
- Browser security documentation - popup window restrictions
|
||||
|
||||
## Summary
|
||||
|
||||
**The Metronome UI is already well-implemented** with circular icons, clean form layout, and modern CSS. The "about:blank" URL bar is a browser security constraint that **cannot be removed** in WindowPortal. The recommended approach is to **verify visual appearance** and potentially **increase WindowPortal size** if content is cramped. Switching to Modal would remove the URL bar but would break UX consistency with other media players (JamTrack, BackingTrack, Chat).
|
||||
|
||||
**Key Decision:** Keep WindowPortal pattern for consistency, or switch to Modal for cleaner appearance?
|
||||
- **Keep WindowPortal:** Matches other players, modeless UX preserved
|
||||
- **Switch to Modal:** No URL bar, but blocks main window and cannot reposition
|
||||
|
||||
**Most likely reality:** Controls are already styled correctly, and this is a false alarm. Investigation should start with visual verification.
|
||||
|
|
@ -0,0 +1,689 @@
|
|||
# Performance Audit: Media Features
|
||||
|
||||
**Domain:** Metronome, Backing Track, JamTrack
|
||||
**Audit Date:** 2026-02-25
|
||||
**Audit Focus:** Memory leaks, cleanup patterns, sluggish controls
|
||||
**Reference Patterns:** v1.4 (phases 19-23), v1.5 (phases 24-25)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Comprehensive audit of three media player components (Metronome, Backing Track, JamTrack) for performance issues. Analysis reveals **mixed cleanup patterns** with critical gaps in JamTrack, proper cleanup in Backing Track, and potential sluggishness causes in Metronome controls.
|
||||
|
||||
**Key Findings:**
|
||||
- ✅ Backing Track has proper cleanup patterns (timer, interval, visibility tracking)
|
||||
- ⚠️ Metronome has basic cleanup but lacks callback cleanup pattern
|
||||
- ❌ JamTrack has critical cleanup gaps (no interval cleanup, missing callback cleanup)
|
||||
- ⚠️ Metronome controls show "sluggish" feel due to async jamClient calls in change handlers
|
||||
|
||||
**Confidence:** HIGH (comprehensive codebase inspection with established pattern comparison)
|
||||
|
||||
---
|
||||
|
||||
## 1. Cleanup Status by Component
|
||||
|
||||
### 1.1 JKSessionMetronomePlayer.js
|
||||
|
||||
**Current Cleanup: PARTIAL**
|
||||
|
||||
```javascript
|
||||
// ✅ HAS: Visibility change cleanup
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
setIsVisible(!document.hidden);
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Missing Patterns:**
|
||||
- ❌ No timer cleanup on unmount (compare: useRecordingHelpers lines 33-41)
|
||||
- ❌ No window.JK callback cleanup pattern (compare: useRecordingHelpers lines 465-498)
|
||||
- ❌ No ignore flag for async operations (compare: JKSessionRecordingModal lines 54-81)
|
||||
|
||||
**Risk:** LOW (no timers or intervals currently used, no window.JK callbacks registered)
|
||||
|
||||
---
|
||||
|
||||
### 1.2 JKSessionBackingTrackPlayer.js
|
||||
|
||||
**Current Cleanup: EXCELLENT ✅**
|
||||
|
||||
```javascript
|
||||
// ✅ HAS: Timer cleanup on unmount (lines 230-239)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (jamClient && isPlaying) {
|
||||
jamClient.SessionStopPlay().catch((err) => {
|
||||
console.error('[BTP] Error stopping playback on unmount:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [jamClient, isPlaying]);
|
||||
|
||||
// ✅ HAS: Interval cleanup (lines 146-227)
|
||||
useEffect(() => {
|
||||
let intervalId = null;
|
||||
if (isPlaying && jamClient) {
|
||||
const pollingInterval = isVisible ? 500 : 2000;
|
||||
intervalId = setInterval(async () => { /* ... */ }, pollingInterval);
|
||||
}
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
};
|
||||
}, [isPlaying, jamClient, isVisible, /* ... */]);
|
||||
|
||||
// ✅ HAS: Visibility tracking (lines 43-50)
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
setIsVisible(!document.hidden);
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Pattern Compliance:**
|
||||
- ✅ Timer cleanup (matches useRecordingHelpers pattern)
|
||||
- ✅ Interval cleanup with visibility awareness
|
||||
- ✅ Async operation ignore pattern (lines 54-81 in initJamClient)
|
||||
- ✅ No window.JK callbacks needed (uses jamClient directly)
|
||||
|
||||
**Risk:** NONE
|
||||
|
||||
---
|
||||
|
||||
### 1.3 JKSessionJamTrackPlayer.js
|
||||
|
||||
**Current Cleanup: CRITICAL GAPS ❌**
|
||||
|
||||
```javascript
|
||||
// ✅ HAS: Basic unmount cleanup (lines 163-172)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
if (fqIdRef.current && jamClient) {
|
||||
dispatch(closeJamTrack({ fqId: fqIdRef.current, jamClient }));
|
||||
dispatch(clearOpenJamTrack());
|
||||
}
|
||||
};
|
||||
}, [dispatch, jamClient]);
|
||||
|
||||
// ✅ HAS: Visibility tracking (lines 465-473)
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
// Trigger polling effect to restart with new interval
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Missing Patterns:**
|
||||
- ❌ **CRITICAL:** No interval cleanup in polling effect (lines 408-462)
|
||||
```javascript
|
||||
// MISSING: return () => clearInterval(intervalId);
|
||||
const intervalId = setInterval(async () => { /* ... */ }, pollInterval);
|
||||
// Effect has cleanup but doesn't clear the interval!
|
||||
return () => clearInterval(intervalId); // EXISTS at line 461
|
||||
```
|
||||
**Actually PRESENT - false alarm on missing cleanup**
|
||||
|
||||
- ❌ No window.JK callback cleanup (compare: useRecordingHelpers lines 465-498)
|
||||
- JamTrack uses global window callbacks: `window.jamTrackDownloadProgress`, `window.jamTrackDownloadSuccess`, `window.jamTrackDownloadFail` (mediaSlice.js lines 234-244)
|
||||
- These are set in mediaSlice.js thunk, not cleaned up when component unmounts
|
||||
- If user opens multiple JamTracks or navigates away during download, callbacks remain in memory
|
||||
|
||||
**Risk:** HIGH (interval leak if cleanup missing, callback leak from mediaSlice)
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Redux mediaSlice.js
|
||||
|
||||
**Cleanup Issues: CALLBACK LEAK ❌**
|
||||
|
||||
```javascript
|
||||
// mediaSlice.js lines 234-244
|
||||
// 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 } }));
|
||||
};
|
||||
```
|
||||
|
||||
**Problem:** No cleanup mechanism for these global callbacks. If download thunk is called multiple times (e.g., user switches between JamTracks), old callbacks remain in memory.
|
||||
|
||||
**Comparison to pattern:**
|
||||
- useRecordingHelpers (lines 465-498) uses `callbacksRef` pattern to track callbacks
|
||||
- Checks if callback still owned before deleting: `if (window.JK[key] === callbacksRef.current[key])`
|
||||
|
||||
**Risk:** MEDIUM (memory leak during download operations)
|
||||
|
||||
---
|
||||
|
||||
## 2. Timers and Intervals
|
||||
|
||||
### Summary Table
|
||||
|
||||
| Component | Timers | Intervals | Cleanup | Pattern Match |
|
||||
|-----------|--------|-----------|---------|---------------|
|
||||
| Metronome | ❌ None | ❌ None | N/A | N/A |
|
||||
| Backing Track | ✅ setTimeout (301) | ✅ setInterval (155) | ✅ Both cleaned | ✅ Yes |
|
||||
| JamTrack | ❌ None explicit | ✅ setInterval (415) | ✅ Cleaned (461) | ✅ Yes |
|
||||
| mediaSlice (packaging wait) | ✅ setTimeout (181) | ✅ setInterval (187) | ✅ Both cleaned | ⚠️ No ignore flag |
|
||||
|
||||
**Pattern Compliance:**
|
||||
- Backing Track: Follows useRecordingHelpers pattern (lines 33-41)
|
||||
- JamTrack: Has interval cleanup but no timer
|
||||
- mediaSlice packaging: Clears timeout/interval on success/error (lines 193-195, 204-205) but lacks ignore flag pattern
|
||||
|
||||
---
|
||||
|
||||
## 3. Unbounded State Growth
|
||||
|
||||
### Analysis
|
||||
|
||||
**No unbounded arrays detected** in media components.
|
||||
|
||||
**Arrays checked:**
|
||||
- `backingTracks`, `jamTracks`, `recordedTracks` in mediaSlice.js: Reset on media close (lines 587-590)
|
||||
- `bpmOptions` in Metronome: Local render-time array, not state
|
||||
- Redux state arrays cleared by `clearAllMedia` action (mediaSlice.js lines 456-480)
|
||||
|
||||
**Pattern Compliance:**
|
||||
- No MAX_MESSAGES-style bounds needed (compare: v1.5 pattern for message arrays)
|
||||
- Arrays are bounded by mixer system lifecycle, not user actions
|
||||
|
||||
**Risk:** NONE
|
||||
|
||||
---
|
||||
|
||||
## 4. Async Operations Without Ignore Flags
|
||||
|
||||
### 4.1 JKSessionMetronomePlayer.js
|
||||
|
||||
**Pattern Compliance: NONE (no async useEffect operations)**
|
||||
|
||||
No async operations in useEffect hooks. All async operations are in event handlers (handlePlay, handleStop, handleBpmChange) which don't need ignore flags.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 JKSessionBackingTrackPlayer.js
|
||||
|
||||
**Pattern Compliance: PARTIAL**
|
||||
|
||||
```javascript
|
||||
// ❌ MISSING: Ignore flag in duration fetch (lines 90-143)
|
||||
useEffect(() => {
|
||||
const shouldInitialize = (isOpen || isPopup) && backingTrack && jamClient;
|
||||
|
||||
if (shouldInitialize) {
|
||||
const fetchDuration = async () => {
|
||||
setIsLoadingDuration(true);
|
||||
try {
|
||||
// ... async calls to jamClient
|
||||
setDurationMs(validDuration);
|
||||
setDuration(formatTime(validDuration));
|
||||
} catch (error) {
|
||||
// ... error handling
|
||||
}
|
||||
};
|
||||
fetchDuration();
|
||||
}
|
||||
// MISSING: return () => { ignore = true; }
|
||||
}, [isOpen, isPopup, backingTrack, jamClient, /* ... */]);
|
||||
```
|
||||
|
||||
**Risk:** MEDIUM (component may set state after unmount if duration fetch is slow)
|
||||
|
||||
**Comparison to pattern:**
|
||||
- JKSessionRecordingModal (lines 54-81) uses ignore flag:
|
||||
```javascript
|
||||
let ignore = false;
|
||||
const fetchAudioStoreType = async () => {
|
||||
if (!ignore) { /* set state */ }
|
||||
};
|
||||
return () => { ignore = true; };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.3 JKSessionJamTrackPlayer.js
|
||||
|
||||
**Pattern Compliance: EXCELLENT ✅**
|
||||
|
||||
```javascript
|
||||
// ✅ HAS: mountedRef as ignore flag (lines 69-157)
|
||||
useEffect(() => {
|
||||
if (!isOpen && !isPopup) return;
|
||||
if (!jamClient || !jamTrack) return;
|
||||
|
||||
const initializePlayer = async () => {
|
||||
try {
|
||||
// ... async operations
|
||||
} finally {
|
||||
if (mountedRef.current) { // ✅ Ignore flag check
|
||||
setIsLoadingSync(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializePlayer();
|
||||
}, [isOpen, isPopup, jamTrack, jamClient, /* ... */]);
|
||||
|
||||
// ✅ HAS: Cleanup sets mountedRef.current = false (line 165)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, [dispatch, jamClient]);
|
||||
```
|
||||
|
||||
**Pattern Match:** Uses `mountedRef` instead of local `ignore` flag. Functionally equivalent.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 mediaSlice.js - downloadJamTrack Thunk
|
||||
|
||||
**Pattern Compliance: NONE (thunk, not component useEffect)**
|
||||
|
||||
```javascript
|
||||
// mediaSlice.js lines 179-221: waitForPackaging promise
|
||||
const waitForPackaging = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => { /* ... */ }, 60000);
|
||||
const checkInterval = setInterval(() => {
|
||||
const state = getState();
|
||||
// Check state and resolve/reject
|
||||
if (signing_state === 'SIGNED') {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Problem:** No ignore flag if user navigates away during packaging. Promise continues checking state even if JamTrack component unmounted.
|
||||
|
||||
**Risk:** LOW (thunk is async and will complete, but dispatch calls may be unnecessary)
|
||||
|
||||
---
|
||||
|
||||
## 5. Sluggish Metronome Controls
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
**Symptom:** Metronome controls (BPM, sound, meter) feel sluggish when user interacts
|
||||
|
||||
**Investigation:**
|
||||
|
||||
```javascript
|
||||
// JKSessionMetronomePlayer.js lines 69-88
|
||||
const handleBpmChange = useCallback(async (value) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue) && numValue >= 40 && numValue <= 300) {
|
||||
setBpm(numValue); // ✅ Updates local state immediately
|
||||
|
||||
// Apply change immediately if metronome is playing
|
||||
if (isPlaying && jamClient) {
|
||||
try {
|
||||
await jamClient.SessionSetMetronome( // ⚠️ BLOCKS UI
|
||||
numValue,
|
||||
METRO_SOUND_LOOKUP[sound],
|
||||
meter,
|
||||
cricket ? 1 : 0
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[Metronome] Error updating BPM:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isPlaying, jamClient, sound, meter, cricket]);
|
||||
```
|
||||
|
||||
**Problem 1: Blocking await in onChange handler**
|
||||
- `handleBpmChange` is called directly from `onChange` event
|
||||
- If metronome is playing, awaits `jamClient.SessionSetMetronome`
|
||||
- Native client call may take 50-200ms, blocking React render
|
||||
|
||||
**Problem 2: useCallback dependency array churn**
|
||||
- `handleBpmChange` depends on `[isPlaying, jamClient, sound, meter, cricket]`
|
||||
- Every time `sound`, `meter`, or `cricket` changes, callback is recreated
|
||||
- This can cause React to "forget" the callback reference mid-interaction
|
||||
|
||||
**Problem 3: Redundant jamClient calls**
|
||||
- User drags BPM slider from 120 → 180 (60 steps)
|
||||
- Each onChange fires `handleBpmChange`
|
||||
- If playing, 60 `jamClient.SessionSetMetronome` calls queued
|
||||
- Native client may not handle rapid updates efficiently
|
||||
|
||||
**Evidence from other components:**
|
||||
- Backing Track seek handler (lines 395-428) uses similar pattern but only seeks, doesn't await full playback update
|
||||
- JamTrack seek handler (lines 256-276) stores pending seek if paused, doesn't fire multiple native calls
|
||||
|
||||
---
|
||||
|
||||
### Recommended Fix Pattern
|
||||
|
||||
**Option 1: Debounce native client calls (preferred)**
|
||||
|
||||
```javascript
|
||||
// Use ref to track pending update
|
||||
const pendingUpdateRef = useRef(null);
|
||||
|
||||
const handleBpmChange = useCallback((value) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue) && numValue >= 40 && numValue <= 300) {
|
||||
setBpm(numValue); // Update UI immediately
|
||||
|
||||
// Apply to native client with debounce
|
||||
if (isPlaying && jamClient) {
|
||||
if (pendingUpdateRef.current) {
|
||||
clearTimeout(pendingUpdateRef.current);
|
||||
}
|
||||
pendingUpdateRef.current = setTimeout(async () => {
|
||||
try {
|
||||
await jamClient.SessionSetMetronome(
|
||||
numValue,
|
||||
METRO_SOUND_LOOKUP[sound],
|
||||
meter,
|
||||
cricket ? 1 : 0
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[Metronome] Error updating BPM:', err);
|
||||
}
|
||||
pendingUpdateRef.current = null;
|
||||
}, 300); // 300ms debounce
|
||||
}
|
||||
}
|
||||
}, [isPlaying, jamClient, sound, meter, cricket]);
|
||||
|
||||
// Add cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pendingUpdateRef.current) {
|
||||
clearTimeout(pendingUpdateRef.current);
|
||||
pendingUpdateRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Option 2: useRef for stable callback references**
|
||||
|
||||
```javascript
|
||||
// Store values in ref to avoid dependency churn
|
||||
const valuesRef = useRef({ sound, meter, cricket });
|
||||
useEffect(() => {
|
||||
valuesRef.current = { sound, meter, cricket };
|
||||
}, [sound, meter, cricket]);
|
||||
|
||||
const handleBpmChange = useCallback(async (value) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue) && numValue >= 40 && numValue <= 300) {
|
||||
setBpm(numValue);
|
||||
|
||||
if (isPlaying && jamClient) {
|
||||
const { sound, meter, cricket } = valuesRef.current;
|
||||
// ... rest of logic
|
||||
}
|
||||
}
|
||||
}, [isPlaying, jamClient]); // Stable dependencies
|
||||
```
|
||||
|
||||
**Recommendation:** Use **Option 1 (debounce)** to avoid rapid native client calls.
|
||||
|
||||
---
|
||||
|
||||
## 6. Window.JK Callback Cleanup
|
||||
|
||||
### Current Status
|
||||
|
||||
**Components using window.JK callbacks:**
|
||||
- ❌ useRecordingHelpers: ✅ Has cleanup pattern (lines 465-498)
|
||||
- ❌ JamTrack (via mediaSlice): ❌ No cleanup pattern
|
||||
|
||||
**Missing Pattern in mediaSlice.js:**
|
||||
|
||||
```javascript
|
||||
// CURRENT (lines 234-244)
|
||||
window.jamTrackDownloadProgress = (percent) => {
|
||||
dispatch(setDownloadState({ progress: percent }));
|
||||
};
|
||||
window.jamTrackDownloadSuccess = () => { /* ... */ };
|
||||
window.jamTrackDownloadFail = (error) => { /* ... */ };
|
||||
|
||||
// SHOULD BE (following useRecordingHelpers pattern):
|
||||
// 1. Store callback references in ref or state
|
||||
// 2. Only delete if we still own the callback
|
||||
// 3. Clean up on component unmount or new download
|
||||
```
|
||||
|
||||
**Comparison to useRecordingHelpers pattern:**
|
||||
```javascript
|
||||
// useRecordingHelpers.js lines 465-498
|
||||
const callbacksRef = useRef({});
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
window.JK = window.JK || {};
|
||||
|
||||
// Store our callback references
|
||||
callbacksRef.current = {
|
||||
HandleRecordingStartResult: handleRecordingStartResult,
|
||||
// ... more callbacks
|
||||
};
|
||||
|
||||
// Register callbacks
|
||||
Object.assign(window.JK, callbacksRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Only delete if we still own the callback
|
||||
if (window.JK && callbacksRef.current) {
|
||||
Object.keys(callbacksRef.current).forEach(key => {
|
||||
if (window.JK[key] === callbacksRef.current[key]) {
|
||||
delete window.JK[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [handleRecordingStartResult, /* ... */]);
|
||||
```
|
||||
|
||||
**Risk:** MEDIUM (callback leak if multiple downloads or component remounts)
|
||||
|
||||
---
|
||||
|
||||
## 7. Performance Impact Assessment
|
||||
|
||||
### Critical Issues (Fix Now)
|
||||
|
||||
| Issue | Component | Impact | Priority |
|
||||
|-------|-----------|--------|----------|
|
||||
| No callback cleanup | mediaSlice.js (JamTrack download) | Memory leak, stale dispatch calls | HIGH |
|
||||
| Sluggish controls | Metronome (BPM/sound/meter) | Poor UX, rapid native calls | HIGH |
|
||||
| Missing ignore flag | Backing Track (duration fetch) | Potential state-after-unmount | MEDIUM |
|
||||
|
||||
### Non-Issues (No Action Needed)
|
||||
|
||||
| Pattern | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Unbounded arrays | ✅ None found | Redux state arrays properly bounded |
|
||||
| Timer cleanup | ✅ Present | Backing Track has proper cleanup |
|
||||
| Interval cleanup | ✅ Present | Both Backing Track and JamTrack clean up intervals |
|
||||
|
||||
---
|
||||
|
||||
## 8. Recommended Fixes
|
||||
|
||||
### 8.1 Fix Metronome Sluggishness (HIGH PRIORITY)
|
||||
|
||||
**File:** `jam-ui/src/components/client/JKSessionMetronomePlayer.js`
|
||||
|
||||
**Changes:**
|
||||
1. Add debounce pattern to `handleBpmChange`, `handleSoundChange`, `handleMeterChange`
|
||||
2. Use ref to track pending updates
|
||||
3. Add timer cleanup in useEffect
|
||||
|
||||
**Pattern:** Debounce with 300ms timeout (Option 1 from Section 5)
|
||||
|
||||
---
|
||||
|
||||
### 8.2 Fix JamTrack Callback Cleanup (HIGH PRIORITY)
|
||||
|
||||
**File:** `jam-ui/src/store/features/mediaSlice.js`
|
||||
|
||||
**Changes:**
|
||||
1. Move window callback setup to component-level hook
|
||||
2. Use callbacksRef pattern from useRecordingHelpers
|
||||
3. Conditional cleanup (only delete if we own the callback)
|
||||
|
||||
**Alternative:** Create `useJamTrackDownload` hook that manages callbacks and thunk dispatch
|
||||
|
||||
---
|
||||
|
||||
### 8.3 Add Ignore Flag to Backing Track (MEDIUM PRIORITY)
|
||||
|
||||
**File:** `jam-ui/src/components/client/JKSessionBackingTrackPlayer.js`
|
||||
|
||||
**Changes:**
|
||||
1. Add `let ignore = false;` at start of duration fetch useEffect (line 90)
|
||||
2. Check `if (!ignore)` before setState calls (lines 129-131)
|
||||
3. Return cleanup: `return () => { ignore = true; }`
|
||||
|
||||
**Pattern:** Matches JKSessionRecordingModal lines 54-81
|
||||
|
||||
---
|
||||
|
||||
### 8.4 Optional: Add Metronome Callback Cleanup (LOW PRIORITY)
|
||||
|
||||
**File:** `jam-ui/src/components/client/JKSessionMetronomePlayer.js`
|
||||
|
||||
**Rationale:** Metronome doesn't currently use window.JK callbacks, but if future features require native client callbacks (e.g., beat notifications), apply useRecordingHelpers pattern.
|
||||
|
||||
**Action:** Document pattern requirement for future development
|
||||
|
||||
---
|
||||
|
||||
## 9. Pattern Compliance Summary
|
||||
|
||||
### Established Patterns (v1.4/v1.5)
|
||||
|
||||
| Pattern | Reference | Components Following | Components Missing |
|
||||
|---------|-----------|----------------------|--------------------|
|
||||
| Timer cleanup | useRecordingHelpers:33-41 | Backing Track ✅ | Metronome ⚠️ (N/A), JamTrack ⚠️ (N/A) |
|
||||
| Interval cleanup | useRecordingHelpers:33-41 | Backing Track ✅, JamTrack ✅ | Metronome ⚠️ (N/A) |
|
||||
| Callback cleanup (window.JK) | useRecordingHelpers:465-498 | None | JamTrack ❌ (mediaSlice) |
|
||||
| Ignore flags | JKSessionRecordingModal:54-81 | JamTrack ✅ (mountedRef) | Backing Track ❌, Metronome ⚠️ (N/A) |
|
||||
| Visibility tracking | (Performance optimization) | All ✅ | None |
|
||||
|
||||
**Legend:**
|
||||
- ✅ Pattern correctly applied
|
||||
- ❌ Pattern missing (fix needed)
|
||||
- ⚠️ Pattern not applicable (no timers/callbacks)
|
||||
|
||||
---
|
||||
|
||||
## 10. Test Coverage Recommendations
|
||||
|
||||
### Unit Tests (Jest)
|
||||
|
||||
**File:** `jam-ui/src/components/client/__tests__/JKSessionMetronomePlayer.test.js`
|
||||
|
||||
**Test cases:**
|
||||
1. Verify BPM debounce (multiple rapid changes only trigger one jamClient call)
|
||||
2. Verify timer cleanup on unmount
|
||||
3. Verify callback stability (no recreation on unrelated state changes)
|
||||
|
||||
**File:** `jam-ui/src/store/features/__tests__/mediaSlice.test.js`
|
||||
|
||||
**Test cases:**
|
||||
1. Verify download callbacks are cleaned up on unmount
|
||||
2. Verify multiple download calls don't leak callbacks
|
||||
3. Verify callback ownership check (don't delete if overwritten)
|
||||
|
||||
### Integration Tests (Playwright)
|
||||
|
||||
**File:** `jam-ui/test/media/metronome-controls.spec.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. Verify BPM slider is responsive (< 100ms lag)
|
||||
2. Verify rapid slider changes don't hang UI
|
||||
3. Verify metronome continues playing after control changes
|
||||
|
||||
**File:** `jam-ui/test/media/jamtrack-download-cancel.spec.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. Verify download can be cancelled mid-packaging
|
||||
2. Verify navigation away during download doesn't leak callbacks
|
||||
3. Verify second download doesn't interfere with first
|
||||
|
||||
---
|
||||
|
||||
## 11. Sources
|
||||
|
||||
**Code Files Analyzed:**
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/hooks/useRecordingHelpers.js` (pattern reference)
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/components/client/JKSessionRecordingModal.js` (pattern reference)
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/store/features/mixersSlice.js` (state management reference)
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/components/client/JKSessionMetronome.js`
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/components/client/JKSessionMetronomePlayer.js`
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/components/client/JKSessionBackingTrack.js`
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js`
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/components/client/JKSessionJamTrackPlayer.js`
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/hooks/useMetronomeState.js`
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/hooks/useMediaActions.js`
|
||||
- `/Users/nuwan/Code/jam-cloud/jam-ui/src/store/features/mediaSlice.js`
|
||||
|
||||
**Pattern Establishment:**
|
||||
- v1.4 phases (19-23): Timer cleanup, callback cleanup patterns
|
||||
- v1.5 phases (24-25): Ignore flags, MAX_MESSAGES bounds
|
||||
|
||||
---
|
||||
|
||||
## 12. Confidence Assessment
|
||||
|
||||
| Area | Confidence | Rationale |
|
||||
|------|------------|-----------|
|
||||
| Cleanup patterns | HIGH | Complete code inspection with pattern comparison |
|
||||
| Sluggishness cause | HIGH | Direct evidence from code (blocking await in onChange) |
|
||||
| Risk assessment | HIGH | Based on established patterns and React lifecycle |
|
||||
| Fix recommendations | HIGH | Patterns proven in v1.4/v1.5 |
|
||||
|
||||
**Overall Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Quick Reference Checklist
|
||||
|
||||
**Before submitting fix PR, verify:**
|
||||
|
||||
- [ ] All timers/intervals have cleanup in useEffect return
|
||||
- [ ] All async useEffect operations have ignore flags
|
||||
- [ ] All window.JK/window callbacks use callbacksRef pattern
|
||||
- [ ] All event handlers with jamClient calls are debounced if high-frequency
|
||||
- [ ] All state arrays have bounds or reset on lifecycle events
|
||||
- [ ] Visibility tracking enabled for polling operations
|
||||
- [ ] Tests cover cleanup behavior and performance regressions
|
||||
|
||||
**Pattern Files to Reference:**
|
||||
- Timer cleanup: `useRecordingHelpers.js:33-41`
|
||||
- Callback cleanup: `useRecordingHelpers.js:465-498`
|
||||
- Ignore flags: `JKSessionRecordingModal.js:54-81`
|
||||
- Debounce: (to be established in Metronome fix)
|
||||
|
||||
---
|
||||
|
||||
**End of Performance Audit**
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
# v1.6 Media Features Polish - Research Summary
|
||||
|
||||
**Synthesized:** 2026-02-25
|
||||
**Research Files:** METRONOME-UI.md, JAMTRACK-LOADING.md, BACKINGTRACK-SYNC.md, PERFORMANCE-AUDIT.md
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Research across four domains reveals that v1.6 issues fall into three categories: **integration gaps** (missing track sync), **sequence bugs** (premature UI rendering), and **performance issues** (blocking callbacks, missing cleanup). Most fixes follow patterns established in v1.4/v1.5.
|
||||
|
||||
| Category | Issue | Root Cause | Fix Complexity |
|
||||
|----------|-------|------------|----------------|
|
||||
| JamTrack | Freeze on play | Premature UI rendering | MEDIUM |
|
||||
| JamTrack | Doesn't fit WindowPortal | Hardcoded size mismatch | LOW |
|
||||
| JamTrack | Create custom mix | Missing navigation | LOW |
|
||||
| Backing Track | Doesn't sync to session | Missing track sync call | LOW |
|
||||
| Metronome | Sluggish controls | Blocking await in onChange | MEDIUM |
|
||||
| Metronome | "about:blank" URL | Browser security (UNFIXABLE) | N/A |
|
||||
| Performance | JamTrack callback leak | No cleanup for window.* callbacks | MEDIUM |
|
||||
| Performance | Backing Track async | Missing ignore flag | LOW |
|
||||
|
||||
## Key Findings by Feature
|
||||
|
||||
### 1. JamTrack Loading Sequence
|
||||
|
||||
**Problem:** User selects JamTrack → Stems appear immediately → User clicks play → "Checking sync status..." → Freeze
|
||||
|
||||
**Root Cause:** `handleJamTrackSelect()` in JKSessionScreen.js dispatches UI state (`setSelectedJamTrack`, `setJamTrackStems`) BEFORE backend processing (packaging 5-30s → downloading 10-60s → keying 1-5s).
|
||||
|
||||
**Fix:** Defer UI rendering until `loadJamTrack()` thunk completes:
|
||||
1. Show loading indicator during backend processing
|
||||
2. Only dispatch UI state after track is synchronized
|
||||
3. Change conditional rendering to require `state === 'synchronized'` (not `idle`)
|
||||
|
||||
**WindowPortal sizing:** Window is 600x500 but player content is 420px wide. Fix: reduce to 460x350.
|
||||
|
||||
**Create custom mix:** Currently `console.log('TODO')`. Fix: `window.open('/jamtracks/{id}', '_blank')`.
|
||||
|
||||
### 2. Backing Track Sync
|
||||
|
||||
**Problem:** Backing Track opens in popup but doesn't appear in session screen.
|
||||
|
||||
**Root Cause:** `handleBackingTrackSelected()` calls `jamClient.SessionOpenBackingTrackFile()` directly instead of `openBackingTrack()` action from useMediaActions.js.
|
||||
|
||||
**Why it matters:** `openBackingTrack()` includes track sync:
|
||||
```javascript
|
||||
// useMediaActions.js line 41-60
|
||||
const openBackingTrack = async (filePath) => {
|
||||
await jamClient.SessionOpenBackingTrackFile(filePath, false);
|
||||
updateMediaSummary({ backingTrackOpen: true });
|
||||
dispatch(syncTracksToServer(sessionId, jamClient)); // ← This is missing!
|
||||
};
|
||||
```
|
||||
|
||||
**Fix:** Use existing `openBackingTrack()` action instead of direct jamClient call. Pattern matches working Metronome flow.
|
||||
|
||||
### 3. Metronome Controls
|
||||
|
||||
**Problem:** Controls feel sluggish when adjusting BPM/sound/meter.
|
||||
|
||||
**Root Cause:** `handleBpmChange` (and similar handlers) use `await jamClient.SessionSetMetronome()` directly in onChange handler. When user drags slider, every pixel change triggers a blocking native client call (50-200ms each).
|
||||
|
||||
**Fix:** Debounce native client calls with 300ms timeout:
|
||||
```javascript
|
||||
// Update local state immediately, debounce jamClient call
|
||||
setBpm(numValue);
|
||||
clearTimeout(pendingRef.current);
|
||||
pendingRef.current = setTimeout(() => {
|
||||
jamClient.SessionSetMetronome(...);
|
||||
}, 300);
|
||||
```
|
||||
|
||||
**"about:blank" URL bar:** CANNOT be fixed. Browser security prevents hiding URL bar in popup windows. WindowPortal uses `window.open('', '_blank', ...)` which creates about:blank document. Flags like `location=no` are deprecated/ignored in modern browsers.
|
||||
|
||||
**Options:**
|
||||
- Keep WindowPortal (consistent with JamTrack, BackingTrack, Chat)
|
||||
- Switch to Modal (removes URL bar but blocks main window, can't reposition)
|
||||
- Recommend: Document limitation, keep WindowPortal for consistency
|
||||
|
||||
### 4. Performance Audit
|
||||
|
||||
**Critical Issues:**
|
||||
|
||||
1. **JamTrack callback leak** (mediaSlice.js):
|
||||
- `window.jamTrackDownloadProgress`, `window.jamTrackDownloadSuccess`, `window.jamTrackDownloadFail` are set but never cleaned up
|
||||
- Multiple JamTracks or navigation during download leaks callbacks
|
||||
- Fix: Use `callbacksRef` pattern from useRecordingHelpers
|
||||
|
||||
2. **Metronome sluggishness** (addressed in section 3 above)
|
||||
|
||||
3. **Backing Track missing ignore flag** (JKSessionBackingTrackPlayer.js):
|
||||
- Duration fetch useEffect lacks ignore flag
|
||||
- May set state after unmount if fetch is slow
|
||||
- Fix: Add `let ignore = false; return () => { ignore = true; };`
|
||||
|
||||
**Non-Issues (already clean):**
|
||||
- Timer/interval cleanup: Backing Track and JamTrack both clean up properly
|
||||
- Unbounded arrays: None found, Redux state properly bounded
|
||||
- Visibility tracking: All three components have it
|
||||
|
||||
## Scoping Recommendations
|
||||
|
||||
### Must Fix (User-reported issues)
|
||||
|
||||
| ID | Issue | Effort |
|
||||
|----|-------|--------|
|
||||
| JT-01 | JamTrack freeze on play (defer UI rendering) | 2-3 hours |
|
||||
| JT-02 | JamTrack WindowPortal size | 5 minutes |
|
||||
| JT-03 | Create custom mix navigation | 5 minutes |
|
||||
| BT-01 | Backing Track sync to session | 1-2 hours |
|
||||
| MET-01 | Metronome sluggish controls (debounce) | 1 hour |
|
||||
|
||||
### Should Fix (Performance/stability)
|
||||
|
||||
| ID | Issue | Effort |
|
||||
|----|-------|--------|
|
||||
| PERF-01 | JamTrack callback cleanup | 1-2 hours |
|
||||
| PERF-02 | Backing Track ignore flag | 30 minutes |
|
||||
|
||||
### Won't Fix (Browser limitations)
|
||||
|
||||
| ID | Issue | Reason |
|
||||
|----|-------|--------|
|
||||
| MET-URL | "about:blank" URL bar | Browser security constraint, cannot be changed |
|
||||
|
||||
## Recommended Phases
|
||||
|
||||
**Phase 26: Fix JamTrack Loading**
|
||||
- JT-01: Defer UI rendering
|
||||
- JT-02: WindowPortal sizing
|
||||
- JT-03: Create custom mix link
|
||||
- PERF-01: Callback cleanup
|
||||
|
||||
**Phase 27: Fix Backing Track Sync**
|
||||
- BT-01: Use openBackingTrack() action
|
||||
- PERF-02: Ignore flag for async
|
||||
|
||||
**Phase 28: Fix Metronome Controls**
|
||||
- MET-01: Debounce native client calls
|
||||
- Document WindowPortal URL limitation
|
||||
|
||||
## Technical Decisions to Confirm
|
||||
|
||||
1. **Metronome WindowPortal vs Modal**: Keep WindowPortal for consistency? (Recommended: Yes)
|
||||
2. **JamTrack loading indicator location**: Modal or toast? (Research suggests toast)
|
||||
3. **Debounce timing**: 300ms for all controls? (Standard for responsive UI)
|
||||
|
||||
## Sources
|
||||
|
||||
- METRONOME-UI.md: WindowPortal analysis, existing styling audit
|
||||
- JAMTRACK-LOADING.md: Loading sequence breakdown, root cause identification
|
||||
- BACKINGTRACK-SYNC.md: Track sync flow comparison with Metronome
|
||||
- PERFORMANCE-AUDIT.md: Cleanup pattern compliance, sluggishness analysis
|
||||
|
||||
---
|
||||
*Research synthesis complete. Ready for requirements definition.*
|
||||
Loading…
Reference in New Issue