diff --git a/App.tsx b/App.tsx index 23e77d4..5a1171f 100644 --- a/App.tsx +++ b/App.tsx @@ -6,6 +6,7 @@ import { Article, PlaybackStatus, PlayerState, VoiceName, AudioSegment, ReaderSe import { MIN_SPEED, MAX_SPEED, SPEED_STEP } from './constants'; import { extractArticleContent, generateSpeechFromText } from './services/geminiService'; import { base64ToUint8Array, createWavBlob } from './services/audioUtils'; +import { createTrackedObjectUrl, revokeAllTrackedObjectUrls, revokeMultipleObjectUrls, revokeTrackedObjectUrl } from './services/objectUrlManager'; import { segmentText } from './services/textUtils'; import { QueueItem } from './components/QueueItem'; import { VoiceSelector } from './components/VoiceSelector'; @@ -47,6 +48,11 @@ export default function App() { return null; }; + const cleanupArticleAudio = (article?: Article) => { + if (!article) return; + revokeMultipleObjectUrls(article.segments.map(seg => seg.audioUrl)); + }; + // -- State Updaters -- const updateArticle = (id: string, updates: Partial
) => { @@ -56,8 +62,13 @@ export default function App() { const updateSegment = (articleId: string, segmentId: string, updates: Partial) => { setQueue(prev => prev.map(article => { if (article.id !== articleId) return article; - const newSegments = article.segments.map(seg => - seg.id === segmentId ? { ...seg, ...updates } : seg + const newSegments = article.segments.map(seg => + seg.id === segmentId ? (() => { + if ('audioUrl' in updates && seg.audioUrl && updates.audioUrl !== seg.audioUrl) { + revokeTrackedObjectUrl(seg.audioUrl); + } + return { ...seg, ...updates }; + })() : seg ); return { ...article, segments: newSegments }; })); @@ -76,7 +87,7 @@ export default function App() { const base64Audio = await generateSpeechFromText(text, voice); const pcmData = base64ToUint8Array(base64Audio); const wavBlob = createWavBlob(pcmData); - const audioUrl = URL.createObjectURL(wavBlob); + const audioUrl = createTrackedObjectUrl(wavBlob); updateSegment(articleId, segmentId, { audioUrl, isLoading: false }); } catch (error) { console.error("Segment generation failed", error); @@ -115,6 +126,7 @@ export default function App() { if (idx <= article.currentSegmentIndex) { return seg; } + revokeTrackedObjectUrl(seg.audioUrl); // Invalidate all future segments return { ...seg, audioUrl: undefined, isLoading: false, hasError: false }; }) @@ -123,12 +135,15 @@ export default function App() { // For inactive articles, invalidate everything return { ...article, - segments: article.segments.map(seg => ({ - ...seg, - audioUrl: undefined, - isLoading: false, - hasError: false - })) + segments: article.segments.map(seg => { + revokeTrackedObjectUrl(seg.audioUrl); + return { + ...seg, + audioUrl: undefined, + isLoading: false, + hasError: false + }; + }) }; })); }, [playerState.currentArticleId]); @@ -373,7 +388,15 @@ export default function App() { audio.addEventListener('ended', handleEnded); return () => audio.removeEventListener('ended', handleEnded); - }, [playerState.currentArticleId, playArticle]); + }, [playerState.currentArticleId, playArticle]); + + useEffect(() => { + return () => { + audioRef.current.pause(); + audioRef.current.src = ''; + revokeAllTrackedObjectUrls(); + }; + }, []); const handleSpeedChange = (newSpeed: number) => { const speed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, newSpeed)); @@ -547,10 +570,15 @@ export default function App() { onRemove={() => { if (playerState.currentArticleId === article.id) { pausePlayback(); - setPlayerState(prev => ({ ...prev, currentArticleId: null })); + setPlayerState(prev => ({ ...prev, currentArticleId: null, isPlaying: false })); + audioRef.current.src = ''; } - setQueue(prev => prev.filter(a => a.id !== article.id)); - if (viewId === article.id) setViewId(null); + setQueue(prev => { + const target = prev.find(a => a.id === article.id); + cleanupArticleAudio(target); + return prev.filter(a => a.id !== article.id); + }); + setViewId(prev => prev === article.id ? null : prev); }} /> diff --git a/services/objectUrlManager.ts b/services/objectUrlManager.ts new file mode 100644 index 0000000..f8bec9e --- /dev/null +++ b/services/objectUrlManager.ts @@ -0,0 +1,24 @@ +const trackedUrls = new Set(); + +export const createTrackedObjectUrl = (blob: Blob): string => { + const url = URL.createObjectURL(blob); + trackedUrls.add(url); + return url; +}; + +export const revokeTrackedObjectUrl = (url?: string) => { + if (!url) return; + if (trackedUrls.has(url)) { + trackedUrls.delete(url); + } + URL.revokeObjectURL(url); +}; + +export const revokeMultipleObjectUrls = (urls: (string | undefined)[]) => { + urls.forEach(revokeTrackedObjectUrl); +}; + +export const revokeAllTrackedObjectUrls = () => { + trackedUrls.forEach(url => URL.revokeObjectURL(url)); + trackedUrls.clear(); +};