feat(TTS): implement pause functionality with state preservation
This commit is contained in:
parent
893f74f038
commit
a8d2e416ca
1 changed files with 68 additions and 4 deletions
|
|
@ -413,6 +413,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
const sentenceAlignmentCacheRef = useRef<Map<string, TTSSentenceAlignment>>(new Map());
|
const sentenceAlignmentCacheRef = useRef<Map<string, TTSSentenceAlignment>>(new Map());
|
||||||
const [currentSentenceAlignment, setCurrentSentenceAlignment] = useState<TTSSentenceAlignment | undefined>();
|
const [currentSentenceAlignment, setCurrentSentenceAlignment] = useState<TTSSentenceAlignment | undefined>();
|
||||||
const [currentWordIndex, setCurrentWordIndex] = useState<number | null>(null);
|
const [currentWordIndex, setCurrentWordIndex] = useState<number | null>(null);
|
||||||
|
const isPlayingRef = useRef(false);
|
||||||
const sentencesRef = useRef<string[]>([]);
|
const sentencesRef = useRef<string[]>([]);
|
||||||
const currentIndexRef = useRef(0);
|
const currentIndexRef = useRef(0);
|
||||||
const setTextRef = useRef<(text: string, options?: boolean | SetTextOptions) => void>(() => { });
|
const setTextRef = useRef<(text: string, options?: boolean | SetTextOptions) => void>(() => { });
|
||||||
|
|
@ -530,6 +531,10 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
currentIndexRef.current = currentIndex;
|
currentIndexRef.current = currentIndex;
|
||||||
}, [currentIndex]);
|
}, [currentIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isPlayingRef.current = isPlaying;
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes text into sentences using the shared NLP utility
|
* Processes text into sentences using the shared NLP utility
|
||||||
*
|
*
|
||||||
|
|
@ -572,14 +577,40 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
setCurrentWordIndex(null);
|
setCurrentWordIndex(null);
|
||||||
}, [activeHowl, clearRateWatchdog]);
|
}, [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
|
* Pauses the current audio playback
|
||||||
* Used for external control of playback state
|
* Used for external control of playback state
|
||||||
*/
|
*/
|
||||||
const pause = useCallback(() => {
|
const pause = useCallback(() => {
|
||||||
abortAudio();
|
pauseActiveHowl();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
}, [abortAudio]);
|
}, [pauseActiveHowl]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to a specific location in the document
|
* Navigates to a specific location in the document
|
||||||
|
|
@ -908,15 +939,31 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
*/
|
*/
|
||||||
const togglePlay = useCallback(() => {
|
const togglePlay = useCallback(() => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
abortAudio();
|
pauseActiveHowl();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure audio is unlocked while we're still in the click/tap handler.
|
// Ensure audio is unlocked while we're still in the click/tap handler.
|
||||||
unlockPlaybackOnUserGesture();
|
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);
|
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';
|
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) {
|
onplayerror: function (this: Howl, soundId, error) {
|
||||||
const actualError = error ?? soundId;
|
const actualError = error ?? soundId;
|
||||||
console.warn('Howl playback error:', actualError);
|
console.warn('Howl playback error:', actualError);
|
||||||
|
|
@ -1551,6 +1611,10 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
|
|
||||||
const howl = await playSentenceWithHowl(sentence, currentIndex);
|
const howl = await playSentenceWithHowl(sentence, currentIndex);
|
||||||
if (howl) {
|
if (howl) {
|
||||||
|
if (!isPlayingRef.current) {
|
||||||
|
playbackInFlightRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
howl.play();
|
howl.play();
|
||||||
}
|
}
|
||||||
}, [sentences, currentIndex, playSentenceWithHowl, voice, effectiveNativeSpeed, configTTSProvider, ttsModel]);
|
}, [sentences, currentIndex, playSentenceWithHowl, voice, effectiveNativeSpeed, configTTSProvider, ttsModel]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue