feat(tts): add pauseActiveHowl helper and improve playback pause/resume handling

Introduce pauseActiveHowl to centralize audio pause logic, ensuring consistent
cleanup and media session state updates. Update pause and togglePlay to use the
new helper, enabling accurate resume of paused audio and improved error
handling. Enhance Howl onpause event to synchronize playback state and clear
timeouts.
This commit is contained in:
Richard R 2026-04-16 15:47:39 -06:00
parent 0db53fbd4e
commit 0c7a845952

View file

@ -413,6 +413,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
const sentenceAlignmentCacheRef = useRef<Map<string, TTSSentenceAlignment>>(new Map());
const [currentSentenceAlignment, setCurrentSentenceAlignment] = useState<TTSSentenceAlignment | undefined>();
const [currentWordIndex, setCurrentWordIndex] = useState<number | null>(null);
const isPlayingRef = useRef(false);
const sentencesRef = useRef<string[]>([]);
const currentIndexRef = useRef(0);
const setTextRef = useRef<(text: string, options?: boolean | SetTextOptions) => void>(() => { });
@ -530,6 +531,10 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
currentIndexRef.current = currentIndex;
}, [currentIndex]);
useEffect(() => {
isPlayingRef.current = isPlaying;
}, [isPlaying]);
/**
* Processes text into sentences using the shared NLP utility
*
@ -572,14 +577,40 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
setCurrentWordIndex(null);
}, [activeHowl, clearRateWatchdog]);
/**
* Pauses the current audio playback while preserving seek position.
*/
const pauseActiveHowl = useCallback(() => {
clearRateWatchdog();
if (activeHowl) {
try {
activeHowl.pause();
} catch (error) {
console.warn('Error pausing audio:', error);
}
}
if (pageTurnTimeoutRef.current) {
clearTimeout(pageTurnTimeoutRef.current);
pageTurnTimeoutRef.current = null;
}
playbackInFlightRef.current = false;
setIsProcessing(false);
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = 'paused';
}
}, [activeHowl, clearRateWatchdog]);
/**
* Pauses the current audio playback
* Used for external control of playback state
*/
const pause = useCallback(() => {
abortAudio();
pauseActiveHowl();
setIsPlaying(false);
}, [abortAudio]);
}, [pauseActiveHowl]);
/**
* Navigates to a specific location in the document
@ -908,15 +939,31 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
*/
const togglePlay = useCallback(() => {
if (isPlaying) {
abortAudio();
pauseActiveHowl();
setIsPlaying(false);
return;
}
// Ensure audio is unlocked while we're still in the click/tap handler.
unlockPlaybackOnUserGesture();
// Resume current sentence if we already have a paused Howl.
if (activeHowl) {
applyPlaybackRateToHowl(activeHowl);
playbackInFlightRef.current = true;
try {
activeHowl.play();
setIsPlaying(true);
return;
} catch (error) {
console.warn('Error resuming audio:', error);
playbackInFlightRef.current = false;
setActiveHowl(null);
}
}
setIsPlaying(true);
}, [abortAudio, isPlaying, unlockPlaybackOnUserGesture]);
}, [activeHowl, applyPlaybackRateToHowl, isPlaying, pauseActiveHowl, unlockPlaybackOnUserGesture]);
/**
@ -1348,6 +1395,19 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
navigator.mediaSession.playbackState = 'playing';
}
},
onpause: function () {
clearRateWatchdog();
playbackInFlightRef.current = false;
setIsProcessing(false);
if (pageTurnTimeoutRef.current) {
clearTimeout(pageTurnTimeoutRef.current);
pageTurnTimeoutRef.current = null;
}
setIsPlaying(false);
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = 'paused';
}
},
onplayerror: function (this: Howl, soundId, error) {
const actualError = error ?? soundId;
console.warn('Howl playback error:', actualError);
@ -1551,6 +1611,10 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
const howl = await playSentenceWithHowl(sentence, currentIndex);
if (howl) {
if (!isPlayingRef.current) {
playbackInFlightRef.current = false;
return;
}
howl.play();
}
}, [sentences, currentIndex, playSentenceWithHowl, voice, effectiveNativeSpeed, configTTSProvider, ttsModel]);