fix(audio): improve Firefox compatibility and audio caching
- Replace base64 data URIs with Blob URLs for better Firefox compatibility - Fixes audio playback issues in Mozilla Firefox - More efficient memory usage and better cross-browser support - Add proper Blob URL cleanup to prevent memory leaks - Enhance audio preloading from 1 to 3 sentences ahead - Reduces waiting time between sentences during TTS playback - Improves overall user experience with smoother audio transitions - Maintains existing cache management and abort control logic Changes: - Add activeBlobUrls ref to track and cleanup Blob URLs - Convert ArrayBuffer to Blob URL instead of base64 data URI in processSentence - Revoke Blob URLs in onend/onstop handlers and abortAudio - Update preloadNextAudio to buffer 3 sentences ahead instead of 1
This commit is contained in:
parent
d5ec96d395
commit
453765076c
1 changed files with 43 additions and 14 deletions
|
|
@ -350,6 +350,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
const preloadRequests = useRef<Map<string, Promise<string>>>(new Map());
|
||||
// Track active abort controllers for TTS requests
|
||||
const activeAbortControllers = useRef<Set<AbortController>>(new Set());
|
||||
// Track active Blob URLs for cleanup
|
||||
const activeBlobUrls = useRef<Set<string>>(new Set());
|
||||
// Track if we're restoring from a saved position
|
||||
const [pendingRestoreIndex, setPendingRestoreIndex] = useState<number | null>(null);
|
||||
// Guard to coalesce rapid restarts and only resume the latest change
|
||||
|
|
@ -405,6 +407,12 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
});
|
||||
activeAbortControllers.current.clear();
|
||||
preloadRequests.current.clear();
|
||||
|
||||
// Revoke all active Blob URLs to prevent memory leaks
|
||||
activeBlobUrls.current.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
activeBlobUrls.current.clear();
|
||||
}
|
||||
|
||||
if (pageTurnTimeoutRef.current) {
|
||||
|
|
@ -931,11 +939,14 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
const audioBuffer = await getAudio(sentence);
|
||||
if (!audioBuffer) throw new Error('No audio data generated');
|
||||
|
||||
// Convert to base64 data URI
|
||||
const bytes = new Uint8Array(audioBuffer);
|
||||
const binaryString = bytes.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
|
||||
const base64String = btoa(binaryString);
|
||||
return `data:audio/mp3;base64,${base64String}`;
|
||||
// Convert to Blob URL (better browser compatibility, especially Firefox)
|
||||
const blob = new Blob([audioBuffer], { type: 'audio/mp3' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// Track Blob URL for cleanup
|
||||
activeBlobUrls.current.add(blobUrl);
|
||||
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
setIsProcessing(false);
|
||||
throw error;
|
||||
|
|
@ -971,9 +982,9 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
|
||||
const createHowl = async (retryCount = 0): Promise<Howl | null> => {
|
||||
try {
|
||||
// Get the processed audio data URI directly from processSentence
|
||||
const audioDataUri = await processSentence(sentence);
|
||||
if (!audioDataUri) {
|
||||
// Get the processed audio Blob URL directly from processSentence
|
||||
const audioBlobUrl = await processSentence(sentence);
|
||||
if (!audioBlobUrl) {
|
||||
throw new Error('No audio data generated');
|
||||
}
|
||||
|
||||
|
|
@ -983,7 +994,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
}
|
||||
|
||||
return new Howl({
|
||||
src: [audioDataUri],
|
||||
src: [audioBlobUrl],
|
||||
format: ['mp3', 'mpeg'],
|
||||
html5: true,
|
||||
preload: true,
|
||||
|
|
@ -1067,6 +1078,13 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
onend: function (this: Howl) {
|
||||
this.unload();
|
||||
setActiveHowl(null);
|
||||
|
||||
// Revoke the Blob URL to free memory
|
||||
if (activeBlobUrls.current.has(audioBlobUrl)) {
|
||||
URL.revokeObjectURL(audioBlobUrl);
|
||||
activeBlobUrls.current.delete(audioBlobUrl);
|
||||
}
|
||||
|
||||
if (pageTurnTimeoutRef.current) {
|
||||
clearTimeout(pageTurnTimeoutRef.current);
|
||||
pageTurnTimeoutRef.current = null;
|
||||
|
|
@ -1078,6 +1096,12 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
onstop: function (this: Howl) {
|
||||
setIsProcessing(false);
|
||||
this.unload();
|
||||
|
||||
// Revoke the Blob URL to free memory
|
||||
if (activeBlobUrls.current.has(audioBlobUrl)) {
|
||||
URL.revokeObjectURL(audioBlobUrl);
|
||||
activeBlobUrls.current.delete(audioBlobUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -1186,12 +1210,17 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
}, [activeHowl, isPlaying, currentSentenceAlignment]);
|
||||
|
||||
/**
|
||||
* Preloads the next sentence's audio
|
||||
* Preloads the next 3 sentences' audio for smoother playback
|
||||
*/
|
||||
const preloadNextAudio = useCallback(async () => {
|
||||
try {
|
||||
const nextSentence = sentences[currentIndex + 1];
|
||||
if (nextSentence) {
|
||||
// Preload the next 3 sentences to ensure smooth playback
|
||||
const PRELOAD_AHEAD_COUNT = 3;
|
||||
|
||||
for (let i = 1; i <= PRELOAD_AHEAD_COUNT; i++) {
|
||||
const nextSentence = sentences[currentIndex + i];
|
||||
if (!nextSentence) break; // Stop if we've reached the end
|
||||
|
||||
const nextKey = buildCacheKey(
|
||||
nextSentence,
|
||||
voice,
|
||||
|
|
@ -1201,9 +1230,9 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
);
|
||||
|
||||
if (!audioCache.has(nextKey) && !preloadRequests.current.has(nextSentence)) {
|
||||
// Start preloading but don't wait for it to complete
|
||||
// Start preloading but don't wait for it to complete
|
||||
processSentence(nextSentence, true).catch(error => {
|
||||
console.error('Error preloading next sentence:', error);
|
||||
console.error(`Error preloading sentence ${i} ahead:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue