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 { 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<Set<AbortController>>(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,

View file

@ -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(),
};

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