Pause in background

This commit is contained in:
Richard Roberson 2025-02-23 04:46:20 -07:00
parent 0be34a09fe
commit 9ad1f5f117
3 changed files with 64 additions and 16 deletions

View file

@ -35,6 +35,7 @@ import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement';
import { useMediaSession } from '@/hooks/audio/useMediaSession'; import { useMediaSession } from '@/hooks/audio/useMediaSession';
import { useAudioContext } from '@/hooks/audio/useAudioContext'; import { useAudioContext } from '@/hooks/audio/useAudioContext';
import { getLastDocumentLocation } from '@/utils/indexedDB'; import { getLastDocumentLocation } from '@/utils/indexedDB';
import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
// Media globals // Media globals
declare global { declare global {
@ -51,6 +52,7 @@ interface TTSContextType {
isPlaying: boolean; isPlaying: boolean;
isProcessing: boolean; isProcessing: boolean;
currentSentence: string; currentSentence: string;
isBackgrounded: boolean; // Add this new property
// Navigation // Navigation
currDocPage: string | number; // Change this to allow both types 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 // Track active abort controllers for TTS requests
const activeAbortControllers = useRef<Set<AbortController>>(new Set()); const activeAbortControllers = useRef<Set<AbortController>>(new Set());
//console.log('page:', currDocPage, 'pages:', currDocPages);
/** /**
* Processes text through the NLP API to split it into sentences * Processes text through the NLP API to split it into sentences
* *
@ -535,7 +535,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
format: ['mp3'], format: ['mp3'],
html5: true, html5: true,
preload: true, preload: true,
pool: 5, // Reduced pool size for iOS compatibility pool: 5,
onplay: () => { onplay: () => {
setIsProcessing(false); setIsProcessing(false);
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
@ -549,7 +549,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
}, },
onend: () => { onend: () => {
URL.revokeObjectURL(audioUrl); URL.revokeObjectURL(audioUrl);
howl.unload(); // Explicitly unload when done howl.unload();
setActiveHowl(null); setActiveHowl(null);
if (isPlaying) { if (isPlaying) {
advance(); advance();
@ -560,19 +560,18 @@ export function TTSProvider({ children }: { children: ReactNode }) {
setIsProcessing(false); setIsProcessing(false);
setActiveHowl(null); setActiveHowl(null);
URL.revokeObjectURL(audioUrl); URL.revokeObjectURL(audioUrl);
howl.unload(); // Ensure cleanup on error howl.unload();
// Don't auto-advance on load error
setIsPlaying(false); setIsPlaying(false);
}, },
onstop: () => { onstop: () => {
setIsProcessing(false); setIsProcessing(false);
URL.revokeObjectURL(audioUrl); URL.revokeObjectURL(audioUrl);
howl.unload(); // Ensure cleanup on stop howl.unload();
} }
}); });
setActiveHowl(howl); setActiveHowl(howl);
howl.play(); return howl;
} catch (error) { } catch (error) {
console.error('Error playing TTS:', error); console.error('Error playing TTS:', error);
@ -588,10 +587,25 @@ export function TTSProvider({ children }: { children: ReactNode }) {
duration: 3000, duration: 3000,
}); });
advance(); // Skip problematic sentence advance();
return null;
} }
}, [isPlaying, processSentence, advance, activeHowl]); }, [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 * Preloads the next sentence's audio
*/ */
@ -609,13 +623,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
} }
}, [currentIndex, sentences, audioCache, processSentence]); }, [currentIndex, sentences, audioCache, processSentence]);
/**
* Plays the current sentence's audio
*/
const playAudio = useCallback(async () => {
await playSentenceWithHowl(sentences[currentIndex]);
}, [sentences, currentIndex, playSentenceWithHowl]);
/** /**
* Main Playback Driver * Main Playback Driver
* Controls the flow of audio playback and sentence processing * 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 (isProcessing) return; // Don't proceed if processing audio
if (!sentences[currentIndex]) return; // Don't proceed if no sentence to play if (!sentences[currentIndex]) return; // Don't proceed if no sentence to play
if (activeHowl) return; // Don't proceed if audio is already playing if (activeHowl) return; // Don't proceed if audio is already playing
if (isBackgrounded) return; // Don't proceed if backgrounded
// Start playing current sentence // Start playing current sentence
playAudio(); playAudio();
@ -644,6 +652,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
currentIndex, currentIndex,
sentences, sentences,
activeHowl, activeHowl,
isBackgrounded,
playAudio, playAudio,
preloadNextAudio, preloadNextAudio,
abortAudio abortAudio
@ -743,6 +752,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
const value = useMemo(() => ({ const value = useMemo(() => ({
isPlaying, isPlaying,
isProcessing, isProcessing,
isBackgrounded,
currentSentence: sentences[currentIndex] || '', currentSentence: sentences[currentIndex] || '',
currDocPage, currDocPage,
currDocPageNumber, currDocPageNumber,
@ -764,6 +774,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
}), [ }), [
isPlaying, isPlaying,
isProcessing, isProcessing,
isBackgrounded,
sentences, sentences,
currentIndex, currentIndex,
currDocPage, currDocPage,

View file

@ -14,6 +14,7 @@ export function useAudioCache(maxSize = 50) {
return { return {
get: (key: string) => cacheRef.current.get(key), get: (key: string) => cacheRef.current.get(key),
set: (key: string, value: ArrayBuffer) => cacheRef.current.set(key, value), set: (key: string, value: ArrayBuffer) => cacheRef.current.set(key, value),
delete: (key: string) => cacheRef.current.delete(key),
has: (key: string) => cacheRef.current.has(key), has: (key: string) => cacheRef.current.has(key),
clear: () => cacheRef.current.clear(), clear: () => cacheRef.current.clear(),
}; };

View file

@ -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;
}