mirror of
https://github.com/Tony0410/News-reader-pro.git
synced 2026-05-24 13:21:40 +08:00
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:
42
App.tsx
42
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<Article>) => {
|
||||
@@ -57,7 +63,12 @@ export default function App() {
|
||||
setQueue(prev => prev.map(article => {
|
||||
if (article.id !== articleId) return article;
|
||||
const newSegments = article.segments.map(seg =>
|
||||
seg.id === segmentId ? { ...seg, ...updates } : 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 => ({
|
||||
segments: article.segments.map(seg => {
|
||||
revokeTrackedObjectUrl(seg.audioUrl);
|
||||
return {
|
||||
...seg,
|
||||
audioUrl: undefined,
|
||||
isLoading: false,
|
||||
hasError: false
|
||||
}))
|
||||
};
|
||||
})
|
||||
};
|
||||
}));
|
||||
}, [playerState.currentArticleId]);
|
||||
@@ -375,6 +390,14 @@ export default function App() {
|
||||
return () => audio.removeEventListener('ended', handleEnded);
|
||||
}, [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));
|
||||
setPlayerState(prev => ({ ...prev, playbackRate: speed }));
|
||||
@@ -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>
|
||||
|
||||
24
services/objectUrlManager.ts
Normal file
24
services/objectUrlManager.ts
Normal 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();
|
||||
};
|
||||
Reference in New Issue
Block a user