diff --git a/ui/src/lib/components/AudioPlayer.svelte b/ui/src/lib/components/AudioPlayer.svelte index ac6df9a..976c39a 100644 --- a/ui/src/lib/components/AudioPlayer.svelte +++ b/ui/src/lib/components/AudioPlayer.svelte @@ -15,9 +15,18 @@ * 4. If 404, POST /api/audio/:slug/:n to generate. Drive pseudo progress bar. * On success, presign again and set audioUrl. * - * ── Pre-fetch at 90% ───────────────────────────────────────────────────── - * A $derived watches currentTime/duration. When >= 90% and autoNext is on - * and there IS a next chapter, it kicks off prefetchNext(): + * ── Pre-fetch (immediate + 90% fallback) ──────────────────────────────── + * When autoNext is on, prefetchNext() is called as soon as the current + * chapter starts playing (via maybeStartPrefetch() at the end of + * startPlayback()). This gives the maximum lead time for Kokoro to + * generate the next chapter so the transition is seamless. + * + * A $effect also watches currentTime/duration and fires prefetchNext() at + * the 90% mark as a fallback — covering the case where autoNext was toggled + * on mid-playback after startPlayback() had already returned. + * The nextStatus !== 'none' guard prevents double-runs in all cases. + * + * prefetchNext(): * • Calls POST /api/audio for next chapter (sets nextStatus='prefetching') * • On success, presigns and stores URL in audioStore.nextAudioUrl * (sets nextStatus='prefetched') @@ -268,6 +277,8 @@ audioStore.audioUrl = url; audioStore.status = 'ready'; // Don't restore saved time for auto-next; position is 0 + // Immediately start pre-generating the chapter after this one. + maybeStartPrefetch(); return; } @@ -278,6 +289,8 @@ audioStore.status = 'ready'; // Restore last saved position after the audio element loads restoreSavedAudioTime(); + // Immediately start pre-generating the next chapter in background. + maybeStartPrefetch(); return; } @@ -299,6 +312,8 @@ audioStore.audioUrl = url2; audioStore.status = 'ready'; // Don't restore time for freshly generated audio — position is 0 + // Immediately start pre-generating the next chapter in background. + maybeStartPrefetch(); } catch (e) { stopProgress(); audioStore.progress = 0; @@ -307,6 +322,25 @@ } } + /** + * Start pre-fetching the next chapter if autoNext is on, there is a next + * chapter, and no prefetch is already running or completed. + * Called as soon as current-chapter playback begins so that the next + * chapter's audio is ready before we need it (seamless transition). + * The 90%-mark $effect acts as a fallback for cases where autoNext is + * toggled on mid-playback. + */ + function maybeStartPrefetch() { + if ( + audioStore.autoNext && + nextChapter !== null && + nextChapter !== undefined && + audioStore.nextStatus === 'none' + ) { + prefetchNext(); + } + } + /** * Fetch the saved audio time for this chapter and seek to it after a short * delay (to allow the audio element to load the source).