Skip to content

Commit 9ad1f5f

Browse files
committed
Pause in background
1 parent 0be34a0 commit 9ad1f5f

File tree

3 files changed

+64
-16
lines changed

3 files changed

+64
-16
lines changed

src/contexts/TTSContext.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement';
3535
import { useMediaSession } from '@/hooks/audio/useMediaSession';
3636
import { useAudioContext } from '@/hooks/audio/useAudioContext';
3737
import { getLastDocumentLocation } from '@/utils/indexedDB';
38+
import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
3839

3940
// Media globals
4041
declare global {
@@ -51,6 +52,7 @@ interface TTSContextType {
5152
isPlaying: boolean;
5253
isProcessing: boolean;
5354
currentSentence: string;
55+
isBackgrounded: boolean; // Add this new property
5456

5557
// Navigation
5658
currDocPage: string | number; // Change this to allow both types
@@ -142,8 +144,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
142144
// Track active abort controllers for TTS requests
143145
const activeAbortControllers = useRef<Set<AbortController>>(new Set());
144146

145-
//console.log('page:', currDocPage, 'pages:', currDocPages);
146-
147147
/**
148148
* Processes text through the NLP API to split it into sentences
149149
*
@@ -535,7 +535,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
535535
format: ['mp3'],
536536
html5: true,
537537
preload: true,
538-
pool: 5, // Reduced pool size for iOS compatibility
538+
pool: 5,
539539
onplay: () => {
540540
setIsProcessing(false);
541541
if ('mediaSession' in navigator) {
@@ -549,7 +549,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
549549
},
550550
onend: () => {
551551
URL.revokeObjectURL(audioUrl);
552-
howl.unload(); // Explicitly unload when done
552+
howl.unload();
553553
setActiveHowl(null);
554554
if (isPlaying) {
555555
advance();
@@ -560,19 +560,18 @@ export function TTSProvider({ children }: { children: ReactNode }) {
560560
setIsProcessing(false);
561561
setActiveHowl(null);
562562
URL.revokeObjectURL(audioUrl);
563-
howl.unload(); // Ensure cleanup on error
564-
// Don't auto-advance on load error
563+
howl.unload();
565564
setIsPlaying(false);
566565
},
567566
onstop: () => {
568567
setIsProcessing(false);
569568
URL.revokeObjectURL(audioUrl);
570-
howl.unload(); // Ensure cleanup on stop
569+
howl.unload();
571570
}
572571
});
573572

574573
setActiveHowl(howl);
575-
howl.play();
574+
return howl;
576575

577576
} catch (error) {
578577
console.error('Error playing TTS:', error);
@@ -588,10 +587,25 @@ export function TTSProvider({ children }: { children: ReactNode }) {
588587
duration: 3000,
589588
});
590589

591-
advance(); // Skip problematic sentence
590+
advance();
591+
return null;
592592
}
593593
}, [isPlaying, processSentence, advance, activeHowl]);
594594

595+
const playAudio = useCallback(async () => {
596+
const howl = await playSentenceWithHowl(sentences[currentIndex]);
597+
if (howl) {
598+
howl.play();
599+
}
600+
}, [sentences, currentIndex, playSentenceWithHowl]);
601+
602+
// Place useBackgroundState after playAudio is defined
603+
const isBackgrounded = useBackgroundState({
604+
activeHowl,
605+
isPlaying,
606+
playAudio,
607+
});
608+
595609
/**
596610
* Preloads the next sentence's audio
597611
*/
@@ -609,13 +623,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
609623
}
610624
}, [currentIndex, sentences, audioCache, processSentence]);
611625

612-
/**
613-
* Plays the current sentence's audio
614-
*/
615-
const playAudio = useCallback(async () => {
616-
await playSentenceWithHowl(sentences[currentIndex]);
617-
}, [sentences, currentIndex, playSentenceWithHowl]);
618-
619626
/**
620627
* Main Playback Driver
621628
* Controls the flow of audio playback and sentence processing
@@ -625,6 +632,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
625632
if (isProcessing) return; // Don't proceed if processing audio
626633
if (!sentences[currentIndex]) return; // Don't proceed if no sentence to play
627634
if (activeHowl) return; // Don't proceed if audio is already playing
635+
if (isBackgrounded) return; // Don't proceed if backgrounded
628636

629637
// Start playing current sentence
630638
playAudio();
@@ -644,6 +652,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
644652
currentIndex,
645653
sentences,
646654
activeHowl,
655+
isBackgrounded,
647656
playAudio,
648657
preloadNextAudio,
649658
abortAudio
@@ -743,6 +752,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
743752
const value = useMemo(() => ({
744753
isPlaying,
745754
isProcessing,
755+
isBackgrounded,
746756
currentSentence: sentences[currentIndex] || '',
747757
currDocPage,
748758
currDocPageNumber,
@@ -764,6 +774,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
764774
}), [
765775
isPlaying,
766776
isProcessing,
777+
isBackgrounded,
767778
sentences,
768779
currentIndex,
769780
currDocPage,

src/hooks/audio/useAudioCache.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function useAudioCache(maxSize = 50) {
1414
return {
1515
get: (key: string) => cacheRef.current.get(key),
1616
set: (key: string, value: ArrayBuffer) => cacheRef.current.set(key, value),
17+
delete: (key: string) => cacheRef.current.delete(key),
1718
has: (key: string) => cacheRef.current.has(key),
1819
clear: () => cacheRef.current.clear(),
1920
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useState, useEffect } from 'react';
2+
import { Howl } from 'howler';
3+
4+
interface UseBackgroundStateProps {
5+
activeHowl: Howl | null;
6+
isPlaying: boolean;
7+
playAudio: () => void;
8+
}
9+
10+
export function useBackgroundState({ activeHowl, isPlaying, playAudio }: UseBackgroundStateProps) {
11+
const [isBackgrounded, setIsBackgrounded] = useState(false);
12+
13+
useEffect(() => {
14+
const handleVisibilityChange = () => {
15+
setIsBackgrounded(document.hidden);
16+
if (document.hidden) {
17+
// When backgrounded, pause audio but maintain isPlaying state
18+
if (activeHowl) {
19+
activeHowl.pause();
20+
}
21+
} else if (isPlaying) {
22+
// When returning to foreground, resume from current position
23+
if (activeHowl) {
24+
activeHowl.play();
25+
}
26+
}
27+
};
28+
29+
document.addEventListener('visibilitychange', handleVisibilityChange);
30+
return () => {
31+
document.removeEventListener('visibilitychange', handleVisibilityChange);
32+
};
33+
}, [isPlaying, activeHowl, playAudio]);
34+
35+
return isBackgrounded;
36+
}

0 commit comments

Comments
 (0)