perf: add adaptive polling and fix lyrics interval leak
- Playback state polling: 1000ms when paused, 250ms when playing - Audio thread timeout: 500ms when idle, 100ms when playing - Lyrics interval: self-check to auto-stop when conditions not met - Add cleanup return to lyrics in +page.svelte - Add build-and-run.sh and monitor-resources.sh scripts - Add OPTIMIZATION_PLAN.md documentation
This commit is contained in:
1 parent
9a817a5203
commit
3e8fe0771a
7 files changed
+442
-6
No files matched your search
@@ -0,0 +1,306 @@
|
|||||||
|
# QBZ Performance Optimization Plan
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
QBZ consumes excessive resources when idle:
|
||||||
|
- **~934MB RAM** (583MB main process + 351MB WebKitWebProcess)
|
||||||
|
- **51 threads**
|
||||||
|
- **~1.5% CPU** without playing audio
|
||||||
|
- User was viewing ArtistDetailView (many images, long page)
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
### Backend (Rust/Tauri)
|
||||||
|
|
||||||
|
| Issue | Location | Impact |
|
||||||
|
|-------|----------|--------|
|
||||||
|
| Playback polling loop | `src-tauri/src/lib.rs:170-206` | 250ms interval, runs ALWAYS |
|
||||||
|
| Audio thread polling | `src-tauri/src/player/mod.rs:498` | 100ms timeout, runs ALWAYS |
|
||||||
|
| Media server HTTP polling | `src-tauri/src/cast/media_server.rs:62` | 250ms timeout, runs ALWAYS |
|
||||||
|
| Audio cache in memory | `src-tauri/src/cache/mod.rs` | 500MB default limit |
|
||||||
|
|
||||||
|
### Frontend (Svelte/WebKit)
|
||||||
|
|
||||||
|
| Issue | Location | Impact |
|
||||||
|
|-------|----------|--------|
|
||||||
|
| **Lyrics 50ms interval** | `src/lib/stores/lyricsStore.ts:389` | 20 updates/sec even when hidden |
|
||||||
|
| IntersectionObserver (4 thresholds) | `src/lib/components/views/ArtistDetailView.svelte:300` | Fires on every scroll |
|
||||||
|
| No lazy loading for images | Multiple components | All images load immediately |
|
||||||
|
| Transform scale on hover (JS) | `src/lib/components/AlbumCard.svelte:37` | Reflows on every hover |
|
||||||
|
| 7 store subscriptions cascade | `src/routes/+page.svelte:1238-1336` | All fire on playback updates |
|
||||||
|
| No list virtualization | QueuePanel, Favorites | Large DOM trees |
|
||||||
|
| backdrop-filter blur | `src/lib/components/glass/GlassSurface.svelte:122` | GPU intensive |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Critical Fixes (Idle CPU)
|
||||||
|
|
||||||
|
### 1.1 Fix Lyrics Interval Leak
|
||||||
|
**File:** `src/lib/stores/lyricsStore.ts`
|
||||||
|
|
||||||
|
**Problem:** 50ms setInterval runs continuously even when lyrics sidebar is hidden.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```typescript
|
||||||
|
// Ensure interval is ONLY running when lyrics are visible AND playing
|
||||||
|
export function startActiveLineUpdates(): void {
|
||||||
|
if (updateInterval !== null) return;
|
||||||
|
// Add visibility check here
|
||||||
|
updateInterval = window.setInterval(() => {
|
||||||
|
if (parsedLyrics.isSynced) {
|
||||||
|
updateActiveLine();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add explicit cleanup
|
||||||
|
export function stopActiveLineUpdates(): void {
|
||||||
|
if (updateInterval !== null) {
|
||||||
|
window.clearInterval(updateInterval);
|
||||||
|
updateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also fix in +page.svelte:** Ensure the $effect properly cleans up on all state changes.
|
||||||
|
|
||||||
|
### 1.2 Conditional Playback Polling
|
||||||
|
**File:** `src-tauri/src/lib.rs:170-206`
|
||||||
|
|
||||||
|
**Problem:** 250ms polling loop runs forever, even when paused.
|
||||||
|
|
||||||
|
**Fix:** Only poll when playing:
|
||||||
|
```rust
|
||||||
|
loop {
|
||||||
|
thread::sleep(Duration::from_millis(250));
|
||||||
|
|
||||||
|
// Skip polling if not playing
|
||||||
|
if !player_handle.is_playing() {
|
||||||
|
thread::sleep(Duration::from_millis(1000)); // Slower poll when paused
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... rest of polling logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Reduce Audio Thread Timeout When Idle
|
||||||
|
**File:** `src-tauri/src/player/mod.rs:498`
|
||||||
|
|
||||||
|
**Problem:** 100ms recv_timeout even when not playing.
|
||||||
|
|
||||||
|
**Fix:** Use longer timeout when idle:
|
||||||
|
```rust
|
||||||
|
let timeout = if is_playing.load(Ordering::Relaxed) {
|
||||||
|
Duration::from_millis(100)
|
||||||
|
} else {
|
||||||
|
Duration::from_millis(500) // Slower when paused
|
||||||
|
};
|
||||||
|
|
||||||
|
match rx.recv_timeout(timeout) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Image Optimization
|
||||||
|
|
||||||
|
### 2.1 Add Lazy Loading to All Images
|
||||||
|
**Files:**
|
||||||
|
- `src/lib/components/AlbumCard.svelte`
|
||||||
|
- `src/lib/components/views/ArtistDetailView.svelte`
|
||||||
|
- `src/lib/components/TrackRow.svelte`
|
||||||
|
|
||||||
|
**Fix:** Add `loading="lazy"` to all `<img>` tags:
|
||||||
|
```svelte
|
||||||
|
<img
|
||||||
|
src={artworkUrl}
|
||||||
|
alt={title}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Use Smaller Image Sizes from Qobuz API
|
||||||
|
**Current:** Using full-size images (e.g., 600x600)
|
||||||
|
**Fix:** Request smaller sizes for thumbnails:
|
||||||
|
- Cards: `?w=300`
|
||||||
|
- Sidebar: `?w=100`
|
||||||
|
- Detail headers: `?w=600`
|
||||||
|
|
||||||
|
### 2.3 Optimize IntersectionObserver in ArtistDetailView
|
||||||
|
**File:** `src/lib/components/views/ArtistDetailView.svelte:300-343`
|
||||||
|
|
||||||
|
**Fix:** Reduce thresholds and debounce:
|
||||||
|
```typescript
|
||||||
|
jumpObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
// Debounce callback
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const visible = entries.find(entry => entry.isIntersecting);
|
||||||
|
if (visible) {
|
||||||
|
const targetId = sectionByElement.get(visible.target as HTMLDivElement);
|
||||||
|
if (targetId) activeJumpSection = targetId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: artistDetailEl,
|
||||||
|
rootMargin: '-20% 0px -60% 0px',
|
||||||
|
threshold: [0.5] // Single threshold instead of 4
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: CSS Performance
|
||||||
|
|
||||||
|
### 3.1 Use CSS :hover Instead of JS State
|
||||||
|
**File:** `src/lib/components/AlbumCard.svelte`
|
||||||
|
|
||||||
|
**Current (Bad):**
|
||||||
|
```svelte
|
||||||
|
<div style="transform: scale({isHovered ? 1.02 : 1})">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix (Good):**
|
||||||
|
```svelte
|
||||||
|
<div class="artwork-container">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.artwork-container {
|
||||||
|
transition: transform 150ms ease-out;
|
||||||
|
}
|
||||||
|
.artwork-container:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Conditional Glass Effects
|
||||||
|
**File:** `src/lib/components/glass/GlassSurface.svelte`
|
||||||
|
|
||||||
|
**Fix:** Add prop to disable blur when not needed:
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
interface Props {
|
||||||
|
enableBlur?: boolean;
|
||||||
|
}
|
||||||
|
let { enableBlur = true }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.glass-backdrop.blur-enabled {
|
||||||
|
backdrop-filter: blur(var(--glass-blur, 16px)) saturate(140%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Memory Optimization
|
||||||
|
|
||||||
|
### 4.1 Reduce Default Audio Cache Size
|
||||||
|
**File:** `src-tauri/src/cache/mod.rs`
|
||||||
|
|
||||||
|
**Current:** 500MB default
|
||||||
|
**Fix:** Reduce to 200MB or make configurable:
|
||||||
|
```rust
|
||||||
|
const DEFAULT_CACHE_SIZE: usize = 200 * 1024 * 1024; // 200MB
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Add Cache Size Setting
|
||||||
|
**New setting in Settings UI:**
|
||||||
|
- Audio cache size: 100MB / 200MB / 500MB / 1GB
|
||||||
|
- Store in settings, pass to AudioCache on init
|
||||||
|
|
||||||
|
### 4.3 Lazy Initialize Cast Services
|
||||||
|
**File:** `src-tauri/src/lib.rs`
|
||||||
|
|
||||||
|
**Current:** Media server starts immediately on app launch.
|
||||||
|
**Fix:** Only start when user opens cast panel:
|
||||||
|
```rust
|
||||||
|
// Don't start in build()
|
||||||
|
// Start on first cast_start_discovery() call instead
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: List Virtualization (Future)
|
||||||
|
|
||||||
|
### 5.1 Virtual Scrolling for QueuePanel
|
||||||
|
**File:** `src/lib/components/QueuePanel.svelte`
|
||||||
|
|
||||||
|
**Implementation:** Use svelte-virtual-list or custom implementation:
|
||||||
|
- Only render visible items + buffer
|
||||||
|
- Recycle DOM nodes on scroll
|
||||||
|
- Target: Handle 1000+ tracks smoothly
|
||||||
|
|
||||||
|
### 5.2 Virtual Scrolling for Favorites
|
||||||
|
Similar implementation for favorites list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
| Priority | Task | Impact | Effort |
|
||||||
|
|----------|------|--------|--------|
|
||||||
|
| P0 | Fix lyrics interval leak | HIGH | LOW |
|
||||||
|
| P0 | Conditional playback polling | HIGH | LOW |
|
||||||
|
| P1 | Lazy load images | HIGH | MEDIUM |
|
||||||
|
| P1 | Reduce IntersectionObserver thresholds | MEDIUM | LOW |
|
||||||
|
| P1 | CSS :hover instead of JS state | MEDIUM | LOW |
|
||||||
|
| P2 | Reduce audio thread timeout when idle | MEDIUM | LOW |
|
||||||
|
| P2 | Reduce default cache size | MEDIUM | LOW |
|
||||||
|
| P2 | Smaller image sizes from API | MEDIUM | MEDIUM |
|
||||||
|
| P3 | Lazy init cast services | LOW | MEDIUM |
|
||||||
|
| P3 | Conditional glass blur | LOW | LOW |
|
||||||
|
| P4 | List virtualization | MEDIUM | HIGH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Before/After Metrics
|
||||||
|
1. Open btop or htop
|
||||||
|
2. Launch QBZ
|
||||||
|
3. Navigate to ArtistDetailView (same artist with many albums)
|
||||||
|
4. Pause playback
|
||||||
|
5. Wait 30 seconds
|
||||||
|
6. Record: CPU%, Memory, Thread count
|
||||||
|
|
||||||
|
### Expected Results After Optimization
|
||||||
|
- **CPU (idle):** < 0.5% (down from 1.5%)
|
||||||
|
- **RAM:** < 600MB total (down from ~934MB)
|
||||||
|
- **Threads:** < 30 (down from 51)
|
||||||
|
|
||||||
|
### Test Cases
|
||||||
|
1. [ ] App idle on home view - CPU should be ~0%
|
||||||
|
2. [ ] App idle on ArtistDetailView - CPU should be < 0.5%
|
||||||
|
3. [ ] Playback paused - CPU should drop to idle levels
|
||||||
|
4. [ ] Scroll through long artist page - no stuttering
|
||||||
|
5. [ ] Open/close lyrics sidebar - no CPU spike after close
|
||||||
|
6. [ ] Memory doesn't grow over time with normal use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `src/lib/stores/lyricsStore.ts` | Fix interval cleanup |
|
||||||
|
| `src-tauri/src/lib.rs` | Conditional polling |
|
||||||
|
| `src-tauri/src/player/mod.rs` | Dynamic recv_timeout |
|
||||||
|
| `src/lib/components/AlbumCard.svelte` | CSS hover, lazy img |
|
||||||
|
| `src/lib/components/views/ArtistDetailView.svelte` | Reduce thresholds, lazy img |
|
||||||
|
| `src/lib/components/TrackRow.svelte` | Lazy img |
|
||||||
|
| `src-tauri/src/cache/mod.rs` | Reduce default size |
|
||||||
|
| `src/lib/components/glass/GlassSurface.svelte` | Conditional blur |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- WebKitWebProcess (351MB) is largely unavoidable - it's the WebView rendering engine
|
||||||
|
- Some memory is WebKit's internal caches and can't be controlled
|
||||||
|
- Focus on CPU optimization first as it's more noticeable to users
|
||||||
|
- Consider adding a "Low Resource Mode" toggle in settings for potato PCs
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build QBZ in release mode and run the binary
|
||||||
|
# Usage: ./scripts/build-and-run.sh [--skip-build]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
BINARY_PATH="$PROJECT_DIR/src-tauri/target/release/qbz-nix"
|
||||||
|
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# Check for --skip-build flag
|
||||||
|
SKIP_BUILD=false
|
||||||
|
if [[ "$1" == "--skip-build" ]]; then
|
||||||
|
SKIP_BUILD=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build if needed
|
||||||
|
if [[ "$SKIP_BUILD" == false ]] || [[ ! -f "$BINARY_PATH" ]]; then
|
||||||
|
echo "=== Building QBZ (release mode) ==="
|
||||||
|
echo "This may take a few minutes..."
|
||||||
|
|
||||||
|
# Use npm run tauri build which handles frontend + backend properly
|
||||||
|
npm run tauri build -- --no-bundle
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
echo "Binary: $BINARY_PATH"
|
||||||
|
echo "Size: $(du -h "$BINARY_PATH" | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kill any existing instance
|
||||||
|
pkill -f "qbz-nix" 2>/dev/null || true
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
echo "=== Running QBZ ==="
|
||||||
|
echo "Monitor with: btop or htop"
|
||||||
|
echo "Press Ctrl+C to stop"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
exec "$BINARY_PATH"
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Monitor QBZ resource usage
|
||||||
|
# Usage: ./scripts/monitor-resources.sh
|
||||||
|
|
||||||
|
echo "=== QBZ Resource Monitor ==="
|
||||||
|
echo "Waiting for QBZ process..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Wait for process to start
|
||||||
|
while ! pgrep -f "qbz-nix" > /dev/null 2>&1; do
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
PID=$(pgrep -f "qbz-nix" | head -1)
|
||||||
|
echo "Found QBZ PID: $PID"
|
||||||
|
echo ""
|
||||||
|
echo "Press Ctrl+C to stop monitoring"
|
||||||
|
echo "==========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Monitor loop
|
||||||
|
while true; do
|
||||||
|
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||||
|
echo "QBZ process ended"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get stats
|
||||||
|
CPU=$(ps -p "$PID" -o %cpu= 2>/dev/null | tr -d ' ')
|
||||||
|
MEM=$(ps -p "$PID" -o rss= 2>/dev/null | tr -d ' ')
|
||||||
|
MEM_MB=$((MEM / 1024))
|
||||||
|
THREADS=$(ps -p "$PID" -o nlwp= 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
# Get WebKit process if exists
|
||||||
|
WEBKIT_PID=$(pgrep -f "WebKitWebProcess" | head -1 2>/dev/null)
|
||||||
|
if [[ -n "$WEBKIT_PID" ]]; then
|
||||||
|
WEBKIT_MEM=$(ps -p "$WEBKIT_PID" -o rss= 2>/dev/null | tr -d ' ')
|
||||||
|
WEBKIT_MB=$((WEBKIT_MEM / 1024))
|
||||||
|
WEBKIT_CPU=$(ps -p "$WEBKIT_PID" -o %cpu= 2>/dev/null | tr -d ' ')
|
||||||
|
else
|
||||||
|
WEBKIT_MB=0
|
||||||
|
WEBKIT_CPU=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOTAL_MB=$((MEM_MB + WEBKIT_MB))
|
||||||
|
|
||||||
|
# Print stats
|
||||||
|
printf "\r[QBZ] CPU: %5s%% | RAM: %4dMB | Threads: %2s | [WebKit] CPU: %5s%% | RAM: %4dMB | TOTAL: %4dMB" \
|
||||||
|
"$CPU" "$MEM_MB" "$THREADS" "$WEBKIT_CPU" "$WEBKIT_MB" "$TOTAL_MB"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
+11
-1
@@ -173,8 +173,18 @@ pub fn run() {
|
|||||||
let mut last_track_id: u64 = 0;
|
let mut last_track_id: u64 = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
// Check playing state first to determine sleep duration
|
||||||
|
let is_playing = player_state.is_playing();
|
||||||
|
|
||||||
|
// Adaptive polling: fast (250ms) when playing, slow (1000ms) when idle
|
||||||
|
let sleep_duration = if is_playing {
|
||||||
|
std::time::Duration::from_millis(250)
|
||||||
|
} else {
|
||||||
|
std::time::Duration::from_millis(1000)
|
||||||
|
};
|
||||||
|
std::thread::sleep(sleep_duration);
|
||||||
|
|
||||||
|
// Re-check after sleep (state might have changed)
|
||||||
let is_playing = player_state.is_playing();
|
let is_playing = player_state.is_playing();
|
||||||
let position = player_state.current_position();
|
let position = player_state.current_position();
|
||||||
let duration = player_state.duration();
|
let duration = player_state.duration();
|
||||||
|
|||||||
@@ -494,8 +494,13 @@ impl Player {
|
|||||||
log::info!("Audio thread ready and waiting for commands");
|
log::info!("Audio thread ready and waiting for commands");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Use recv_timeout so we can periodically check if sink is empty
|
// Adaptive timeout: fast (100ms) when playing, slow (500ms) when idle
|
||||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
let timeout = if thread_state.is_playing.load(Ordering::SeqCst) {
|
||||||
|
Duration::from_millis(100)
|
||||||
|
} else {
|
||||||
|
Duration::from_millis(500)
|
||||||
|
};
|
||||||
|
match rx.recv_timeout(timeout) {
|
||||||
Ok(AudioCommand::Play { data, track_id, duration_secs }) => {
|
Ok(AudioCommand::Play { data, track_id, duration_secs }) => {
|
||||||
log::info!("Audio thread: playing track {}", track_id);
|
log::info!("Audio thread: playing track {}", track_id);
|
||||||
|
|
||||||
|
|||||||
@@ -382,6 +382,14 @@ export function reset(): void {
|
|||||||
// ============ Auto-update ============
|
// ============ Auto-update ============
|
||||||
|
|
||||||
let updateInterval: number | null = null;
|
let updateInterval: number | null = null;
|
||||||
|
let isUpdatesActive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if active line updates are currently running
|
||||||
|
*/
|
||||||
|
export function isActiveLineUpdatesRunning(): boolean {
|
||||||
|
return updateInterval !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start auto-updating active line (call when lyrics are synced and playing)
|
* Start auto-updating active line (call when lyrics are synced and playing)
|
||||||
@@ -389,11 +397,16 @@ let updateInterval: number | null = null;
|
|||||||
export function startActiveLineUpdates(): void {
|
export function startActiveLineUpdates(): void {
|
||||||
if (updateInterval !== null) return;
|
if (updateInterval !== null) return;
|
||||||
|
|
||||||
console.log('[Lyrics] Starting active line updates, isSynced:', parsedLyrics.isSynced);
|
isUpdatesActive = true;
|
||||||
|
console.log('[Lyrics] Starting active line updates');
|
||||||
updateInterval = window.setInterval(() => {
|
updateInterval = window.setInterval(() => {
|
||||||
if (parsedLyrics.isSynced) {
|
// Self-check: stop if no longer needed
|
||||||
updateActiveLine();
|
if (!isUpdatesActive || !parsedLyrics.isSynced) {
|
||||||
|
console.log('[Lyrics] Auto-stopping interval (conditions no longer met)');
|
||||||
|
stopActiveLineUpdates();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
updateActiveLine();
|
||||||
}, 50); // 50ms for smooth progress
|
}, 50); // 50ms for smooth progress
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +414,9 @@ export function startActiveLineUpdates(): void {
|
|||||||
* Stop auto-updating active line
|
* Stop auto-updating active line
|
||||||
*/
|
*/
|
||||||
export function stopActiveLineUpdates(): void {
|
export function stopActiveLineUpdates(): void {
|
||||||
|
isUpdatesActive = false;
|
||||||
if (updateInterval !== null) {
|
if (updateInterval !== null) {
|
||||||
|
console.log('[Lyrics] Stopping active line updates');
|
||||||
clearInterval(updateInterval);
|
clearInterval(updateInterval);
|
||||||
updateInterval = null;
|
updateInterval = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1411,6 +1411,10 @@
|
|||||||
} else {
|
} else {
|
||||||
stopActiveLineUpdates();
|
stopActiveLineUpdates();
|
||||||
}
|
}
|
||||||
|
// Cleanup on effect re-run or component unmount
|
||||||
|
return () => {
|
||||||
|
stopActiveLineUpdates();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derived values for NowPlayingBar
|
// Derived values for NowPlayingBar
|
||||||
|
|||||||
Reference in new issue
Block a user