fix: unstable audio playback

We had convoluted code with audio visualizer, and overall instability in
playing back audio.
This commit is contained in:
talksik
2026-03-12 07:08:33 -07:00
parent 6c20c43773
commit 9a19bba130
2 changed files with 17 additions and 40 deletions
@@ -43,22 +43,17 @@ export function MediaParticleView({
);
const cacheDownloadUrl = usePlaybackStore((s) => s.cacheDownloadUrl);
const paused = usePlaybackStore((s) => s.paused);
const [url, setUrl] = useState<string | null>(cachedUrl ?? null);
const [error, setError] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);
const [audioEl, setAudioEl] = useState<HTMLAudioElement | null>(null);
const [currentTimeMs, setCurrentTimeMs] = useState(0);
const data = particle.data as MediaParticleData;
const isAudio = data.mime_type?.startsWith("audio/");
const audioSource = useAudioSource(isAudio ? audioEl : null);
useEffect(() => {
if (cachedUrl) {
setUrl(cachedUrl);
return;
}
@@ -68,7 +63,6 @@ export function MediaParticleView({
.then((downloadUrl) => {
if (cancelled) return;
cacheDownloadUrl(particle.id, downloadUrl);
setUrl(downloadUrl);
})
.catch(() => {
if (!cancelled) setError("Failed to load media");
@@ -77,40 +71,28 @@ export function MediaParticleView({
return () => {
cancelled = true;
};
}, [particle.id, cachedUrl, cacheDownloadUrl]);
}, [particle.id, cacheDownloadUrl]);
// Start audio playback once the AudioContext source is ready
// Handle pause/resume
useEffect(() => {
const el = audioRef.current;
if (!el || !audioSource) return;
if (!paused) {
el.play().catch(() => {});
var el: HTMLVideoElement | HTMLAudioElement | null = null;
if (isAudio) {
el = audioRef.current;
} else {
el = videoRef.current;
}
}, [audioSource, paused]);
// Handle video pause/resume
useEffect(() => {
const el = videoRef.current;
if (!el) return;
if (paused) {
el.pause();
} else {
el.play().catch(() => {});
el.play().catch(() => {
console.warn("Playback failed", { particleId: particle.id });
});
}
}, [paused]);
// Handle audio pause/resume (after initial play)
useEffect(() => {
const el = audioRef.current;
if (!el || !audioSource) return;
if (paused) {
el.pause();
}
}, [paused, audioSource]);
if (error) {
return (
<div className="text-muted-foreground flex items-center justify-center text-sm">
@@ -119,7 +101,7 @@ export function MediaParticleView({
);
}
if (!url) {
if (!cachedUrl) {
return <Skeleton className="h-full w-full rounded-none" />;
}
@@ -127,22 +109,16 @@ export function MediaParticleView({
return (
<div className="relative flex h-full w-full items-center justify-center bg-black/90">
<audio
ref={(el) => {
audioRef.current = el;
setAudioEl(el);
}}
ref={audioRef}
crossOrigin="anonymous"
src={url}
src={cachedUrl}
autoPlay
onEnded={onEnded}
onTimeUpdate={(e) => {
setCurrentTimeMs(e.currentTarget.currentTime * 1000);
}}
/>
{audioSource && (
<AudioLevelBars sourceNode={audioSource.sourceNode} />
)}
<DurationPill
currentTimeMs={currentTimeMs}
totalDurationMs={data.duration_ms}
@@ -155,7 +131,7 @@ export function MediaParticleView({
<div className="relative h-full w-full">
<video
ref={videoRef}
src={url}
src={cachedUrl}
autoPlay
playsInline
onEnded={onEnded}
@@ -40,7 +40,8 @@ export function ParticleRenderer({
const renderContent = () => {
switch (particle.type) {
case "media":
return <MediaParticleView particle={particle} onEnded={onNext} />;
{/* NOTE: it's more robust to re-mount the MediaParticleView when the particle changes, to ensure playback state is well-behaved */}
return <MediaParticleView key={particle.id} particle={particle} onEnded={onNext} />;
case "text":
return <TextParticleView particle={particle} />;
default: