import { useEffect, useRef } from 'react'; import { usePlaybackStore } from '../store/usePlaybackStore'; import { useVibeStore } from '../store/useVibeStore'; import { trackService } from '../services/trackService'; import { advancePastUnplayableVibeTrack, reportVibeEvent } from '../services/vibeSession'; import type { Track } from '../types'; // Threshold (seconds) above which a store position change is treated as a user // scrub and applied to the audio element. Keeps the timeupdate -> setPosition -> // effect loop from fighting itself. const SEEK_THRESHOLD = 1; // If a track reaches this fraction of its duration, treat it as "effectively // completed" even if the user clicks Next before the very end. const COMPLETION_THRESHOLD = 0.95; // Relative seek increment (seconds) for MediaSession seekforward/seekbackward. const SEEK_INCREMENT = 10; /** Build the artwork URLs for MediaSession metadata (OS media controls). */ function buildArtwork(track: Track): MediaImage[] { const sizes = [96, 128, 192, 256, 384, 512]; const artwork = track.artwork_id; if (!artwork) return []; // artwork_id is either a full URL (external) or a relative path served by us. const url = artwork.startsWith('http') ? artwork : `${window.location.origin}${artwork}`; return sizes.map((s) => ({ src: url, sizes: `${s}x${s}`, type: 'image/jpeg' })); } // Headless audio engine: one shared