Pause in background
This commit is contained in:
parent
0be34a09fe
commit
9ad1f5f117
3 changed files with 64 additions and 16 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
36
src/hooks/audio/useBackgroundState.ts
Normal file
36
src/hooks/audio/useBackgroundState.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue