Merge pull request #1 from Tony0410/codex/track-and-clean-up-audio-urls

Add cleanup for audio segment object URLs
This commit is contained in:
Anthony
2025-11-27 21:19:29 +08:00
committed by GitHub
2 changed files with 65 additions and 13 deletions

54
App.tsx
View File

@@ -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<Article>) => {
@@ -56,8 +62,13 @@ export default function App() {
const updateSegment = (articleId: string, segmentId: string, updates: Partial<AudioSegment>) => {
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);
}}
/>
</div>

View File

@@ -0,0 +1,24 @@
const trackedUrls = new Set<string>();
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();
};