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:
54
App.tsx
54
App.tsx
@@ -6,6 +6,7 @@ import { Article, PlaybackStatus, PlayerState, VoiceName, AudioSegment, ReaderSe
|
|||||||
import { MIN_SPEED, MAX_SPEED, SPEED_STEP } from './constants';
|
import { MIN_SPEED, MAX_SPEED, SPEED_STEP } from './constants';
|
||||||
import { extractArticleContent, generateSpeechFromText } from './services/geminiService';
|
import { extractArticleContent, generateSpeechFromText } from './services/geminiService';
|
||||||
import { base64ToUint8Array, createWavBlob } from './services/audioUtils';
|
import { base64ToUint8Array, createWavBlob } from './services/audioUtils';
|
||||||
|
import { createTrackedObjectUrl, revokeAllTrackedObjectUrls, revokeMultipleObjectUrls, revokeTrackedObjectUrl } from './services/objectUrlManager';
|
||||||
import { segmentText } from './services/textUtils';
|
import { segmentText } from './services/textUtils';
|
||||||
import { QueueItem } from './components/QueueItem';
|
import { QueueItem } from './components/QueueItem';
|
||||||
import { VoiceSelector } from './components/VoiceSelector';
|
import { VoiceSelector } from './components/VoiceSelector';
|
||||||
@@ -47,6 +48,11 @@ export default function App() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cleanupArticleAudio = (article?: Article) => {
|
||||||
|
if (!article) return;
|
||||||
|
revokeMultipleObjectUrls(article.segments.map(seg => seg.audioUrl));
|
||||||
|
};
|
||||||
|
|
||||||
// -- State Updaters --
|
// -- State Updaters --
|
||||||
|
|
||||||
const updateArticle = (id: string, updates: Partial<Article>) => {
|
const updateArticle = (id: string, updates: Partial<Article>) => {
|
||||||
@@ -56,8 +62,13 @@ export default function App() {
|
|||||||
const updateSegment = (articleId: string, segmentId: string, updates: Partial<AudioSegment>) => {
|
const updateSegment = (articleId: string, segmentId: string, updates: Partial<AudioSegment>) => {
|
||||||
setQueue(prev => prev.map(article => {
|
setQueue(prev => prev.map(article => {
|
||||||
if (article.id !== articleId) return article;
|
if (article.id !== articleId) return article;
|
||||||
const newSegments = article.segments.map(seg =>
|
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 };
|
return { ...article, segments: newSegments };
|
||||||
}));
|
}));
|
||||||
@@ -76,7 +87,7 @@ export default function App() {
|
|||||||
const base64Audio = await generateSpeechFromText(text, voice);
|
const base64Audio = await generateSpeechFromText(text, voice);
|
||||||
const pcmData = base64ToUint8Array(base64Audio);
|
const pcmData = base64ToUint8Array(base64Audio);
|
||||||
const wavBlob = createWavBlob(pcmData);
|
const wavBlob = createWavBlob(pcmData);
|
||||||
const audioUrl = URL.createObjectURL(wavBlob);
|
const audioUrl = createTrackedObjectUrl(wavBlob);
|
||||||
updateSegment(articleId, segmentId, { audioUrl, isLoading: false });
|
updateSegment(articleId, segmentId, { audioUrl, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Segment generation failed", error);
|
console.error("Segment generation failed", error);
|
||||||
@@ -115,6 +126,7 @@ export default function App() {
|
|||||||
if (idx <= article.currentSegmentIndex) {
|
if (idx <= article.currentSegmentIndex) {
|
||||||
return seg;
|
return seg;
|
||||||
}
|
}
|
||||||
|
revokeTrackedObjectUrl(seg.audioUrl);
|
||||||
// Invalidate all future segments
|
// Invalidate all future segments
|
||||||
return { ...seg, audioUrl: undefined, isLoading: false, hasError: false };
|
return { ...seg, audioUrl: undefined, isLoading: false, hasError: false };
|
||||||
})
|
})
|
||||||
@@ -123,12 +135,15 @@ export default function App() {
|
|||||||
// For inactive articles, invalidate everything
|
// For inactive articles, invalidate everything
|
||||||
return {
|
return {
|
||||||
...article,
|
...article,
|
||||||
segments: article.segments.map(seg => ({
|
segments: article.segments.map(seg => {
|
||||||
...seg,
|
revokeTrackedObjectUrl(seg.audioUrl);
|
||||||
audioUrl: undefined,
|
return {
|
||||||
isLoading: false,
|
...seg,
|
||||||
hasError: false
|
audioUrl: undefined,
|
||||||
}))
|
isLoading: false,
|
||||||
|
hasError: false
|
||||||
|
};
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
}, [playerState.currentArticleId]);
|
}, [playerState.currentArticleId]);
|
||||||
@@ -373,7 +388,15 @@ export default function App() {
|
|||||||
|
|
||||||
audio.addEventListener('ended', handleEnded);
|
audio.addEventListener('ended', handleEnded);
|
||||||
return () => audio.removeEventListener('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 handleSpeedChange = (newSpeed: number) => {
|
||||||
const speed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, newSpeed));
|
const speed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, newSpeed));
|
||||||
@@ -547,10 +570,15 @@ export default function App() {
|
|||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
if (playerState.currentArticleId === article.id) {
|
if (playerState.currentArticleId === article.id) {
|
||||||
pausePlayback();
|
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));
|
setQueue(prev => {
|
||||||
if (viewId === article.id) setViewId(null);
|
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>
|
</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