diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index 2fde24d..2bd907a 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -35,6 +35,7 @@ import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement'; import { useMediaSession } from '@/hooks/audio/useMediaSession'; import { useAudioContext } from '@/hooks/audio/useAudioContext'; import { getLastDocumentLocation } from '@/utils/indexedDB'; +import { useBackgroundState } from '@/hooks/audio/useBackgroundState'; // Media globals declare global { @@ -51,6 +52,7 @@ interface TTSContextType { isPlaying: boolean; isProcessing: boolean; currentSentence: string; + isBackgrounded: boolean; // Add this new property // Navigation currDocPage: string | number; // Change this to allow both types @@ -142,8 +144,6 @@ export function TTSProvider({ children }: { children: ReactNode }) { // Track active abort controllers for TTS requests const activeAbortControllers = useRef>(new Set()); - //console.log('page:', currDocPage, 'pages:', currDocPages); - /** * Processes text through the NLP API to split it into sentences * @@ -535,7 +535,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { format: ['mp3'], html5: true, preload: true, - pool: 5, // Reduced pool size for iOS compatibility + pool: 5, onplay: () => { setIsProcessing(false); if ('mediaSession' in navigator) { @@ -549,7 +549,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { }, onend: () => { URL.revokeObjectURL(audioUrl); - howl.unload(); // Explicitly unload when done + howl.unload(); setActiveHowl(null); if (isPlaying) { advance(); @@ -560,19 +560,18 @@ export function TTSProvider({ children }: { children: ReactNode }) { setIsProcessing(false); setActiveHowl(null); URL.revokeObjectURL(audioUrl); - howl.unload(); // Ensure cleanup on error - // Don't auto-advance on load error + howl.unload(); setIsPlaying(false); }, onstop: () => { setIsProcessing(false); URL.revokeObjectURL(audioUrl); - howl.unload(); // Ensure cleanup on stop + howl.unload(); } }); setActiveHowl(howl); - howl.play(); + return howl; } catch (error) { console.error('Error playing TTS:', error); @@ -588,10 +587,25 @@ export function TTSProvider({ children }: { children: ReactNode }) { duration: 3000, }); - advance(); // Skip problematic sentence + advance(); + return null; } }, [isPlaying, processSentence, advance, activeHowl]); + const playAudio = useCallback(async () => { + const howl = await playSentenceWithHowl(sentences[currentIndex]); + if (howl) { + howl.play(); + } + }, [sentences, currentIndex, playSentenceWithHowl]); + + // Place useBackgroundState after playAudio is defined + const isBackgrounded = useBackgroundState({ + activeHowl, + isPlaying, + playAudio, + }); + /** * Preloads the next sentence's audio */ @@ -609,13 +623,6 @@ export function TTSProvider({ children }: { children: ReactNode }) { } }, [currentIndex, sentences, audioCache, processSentence]); - /** - * Plays the current sentence's audio - */ - const playAudio = useCallback(async () => { - await playSentenceWithHowl(sentences[currentIndex]); - }, [sentences, currentIndex, playSentenceWithHowl]); - /** * Main Playback Driver * Controls the flow of audio playback and sentence processing @@ -625,6 +632,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { if (isProcessing) return; // Don't proceed if processing audio if (!sentences[currentIndex]) return; // Don't proceed if no sentence to play if (activeHowl) return; // Don't proceed if audio is already playing + if (isBackgrounded) return; // Don't proceed if backgrounded // Start playing current sentence playAudio(); @@ -644,6 +652,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { currentIndex, sentences, activeHowl, + isBackgrounded, playAudio, preloadNextAudio, abortAudio @@ -743,6 +752,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { const value = useMemo(() => ({ isPlaying, isProcessing, + isBackgrounded, currentSentence: sentences[currentIndex] || '', currDocPage, currDocPageNumber, @@ -764,6 +774,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { }), [ isPlaying, isProcessing, + isBackgrounded, sentences, currentIndex, currDocPage, diff --git a/src/hooks/audio/useAudioCache.ts b/src/hooks/audio/useAudioCache.ts index 9901845..096176a 100644 --- a/src/hooks/audio/useAudioCache.ts +++ b/src/hooks/audio/useAudioCache.ts @@ -14,6 +14,7 @@ export function useAudioCache(maxSize = 50) { return { get: (key: string) => cacheRef.current.get(key), set: (key: string, value: ArrayBuffer) => cacheRef.current.set(key, value), + delete: (key: string) => cacheRef.current.delete(key), has: (key: string) => cacheRef.current.has(key), clear: () => cacheRef.current.clear(), }; diff --git a/src/hooks/audio/useBackgroundState.ts b/src/hooks/audio/useBackgroundState.ts new file mode 100644 index 0000000..12c608b --- /dev/null +++ b/src/hooks/audio/useBackgroundState.ts @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react'; +import { Howl } from 'howler'; + +interface UseBackgroundStateProps { + activeHowl: Howl | null; + isPlaying: boolean; + playAudio: () => void; +} + +export function useBackgroundState({ activeHowl, isPlaying, playAudio }: UseBackgroundStateProps) { + const [isBackgrounded, setIsBackgrounded] = useState(false); + + useEffect(() => { + const handleVisibilityChange = () => { + setIsBackgrounded(document.hidden); + if (document.hidden) { + // When backgrounded, pause audio but maintain isPlaying state + if (activeHowl) { + activeHowl.pause(); + } + } else if (isPlaying) { + // When returning to foreground, resume from current position + if (activeHowl) { + activeHowl.play(); + } + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [isPlaying, activeHowl, playAudio]); + + return isBackgrounded; +} \ No newline at end of file