fix: unstable audio playback
We had convoluted code with audio visualizer, and overall instability in playing back audio.
This commit is contained in:
@@ -43,22 +43,17 @@ export function MediaParticleView({
|
|||||||
);
|
);
|
||||||
const cacheDownloadUrl = usePlaybackStore((s) => s.cacheDownloadUrl);
|
const cacheDownloadUrl = usePlaybackStore((s) => s.cacheDownloadUrl);
|
||||||
const paused = usePlaybackStore((s) => s.paused);
|
const paused = usePlaybackStore((s) => s.paused);
|
||||||
const [url, setUrl] = useState<string | null>(cachedUrl ?? null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [audioEl, setAudioEl] = useState<HTMLAudioElement | null>(null);
|
|
||||||
const [currentTimeMs, setCurrentTimeMs] = useState(0);
|
const [currentTimeMs, setCurrentTimeMs] = useState(0);
|
||||||
|
|
||||||
const data = particle.data as MediaParticleData;
|
const data = particle.data as MediaParticleData;
|
||||||
const isAudio = data.mime_type?.startsWith("audio/");
|
const isAudio = data.mime_type?.startsWith("audio/");
|
||||||
|
|
||||||
const audioSource = useAudioSource(isAudio ? audioEl : null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cachedUrl) {
|
if (cachedUrl) {
|
||||||
setUrl(cachedUrl);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +63,6 @@ export function MediaParticleView({
|
|||||||
.then((downloadUrl) => {
|
.then((downloadUrl) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
cacheDownloadUrl(particle.id, downloadUrl);
|
cacheDownloadUrl(particle.id, downloadUrl);
|
||||||
setUrl(downloadUrl);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) setError("Failed to load media");
|
if (!cancelled) setError("Failed to load media");
|
||||||
@@ -77,40 +71,28 @@ export function MediaParticleView({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [particle.id, cachedUrl, cacheDownloadUrl]);
|
}, [particle.id, cacheDownloadUrl]);
|
||||||
|
|
||||||
// Start audio playback once the AudioContext source is ready
|
// Handle pause/resume
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = audioRef.current;
|
var el: HTMLVideoElement | HTMLAudioElement | null = null;
|
||||||
if (!el || !audioSource) return;
|
if (isAudio) {
|
||||||
|
el = audioRef.current;
|
||||||
if (!paused) {
|
} else {
|
||||||
el.play().catch(() => {});
|
el = videoRef.current;
|
||||||
}
|
}
|
||||||
}, [audioSource, paused]);
|
|
||||||
|
|
||||||
// Handle video pause/resume
|
|
||||||
useEffect(() => {
|
|
||||||
const el = videoRef.current;
|
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
if (paused) {
|
if (paused) {
|
||||||
el.pause();
|
el.pause();
|
||||||
} else {
|
} else {
|
||||||
el.play().catch(() => {});
|
el.play().catch(() => {
|
||||||
|
console.warn("Playback failed", { particleId: particle.id });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [paused]);
|
}, [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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-muted-foreground flex items-center justify-center text-sm">
|
<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" />;
|
return <Skeleton className="h-full w-full rounded-none" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,22 +109,16 @@ export function MediaParticleView({
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full items-center justify-center bg-black/90">
|
<div className="relative flex h-full w-full items-center justify-center bg-black/90">
|
||||||
<audio
|
<audio
|
||||||
ref={(el) => {
|
ref={audioRef}
|
||||||
audioRef.current = el;
|
|
||||||
setAudioEl(el);
|
|
||||||
}}
|
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={url}
|
src={cachedUrl}
|
||||||
|
autoPlay
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
onTimeUpdate={(e) => {
|
onTimeUpdate={(e) => {
|
||||||
setCurrentTimeMs(e.currentTarget.currentTime * 1000);
|
setCurrentTimeMs(e.currentTarget.currentTime * 1000);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{audioSource && (
|
|
||||||
<AudioLevelBars sourceNode={audioSource.sourceNode} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DurationPill
|
<DurationPill
|
||||||
currentTimeMs={currentTimeMs}
|
currentTimeMs={currentTimeMs}
|
||||||
totalDurationMs={data.duration_ms}
|
totalDurationMs={data.duration_ms}
|
||||||
@@ -155,7 +131,7 @@ export function MediaParticleView({
|
|||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
src={url}
|
src={cachedUrl}
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export function ParticleRenderer({
|
|||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (particle.type) {
|
switch (particle.type) {
|
||||||
case "media":
|
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":
|
case "text":
|
||||||
return <TextParticleView particle={particle} />;
|
return <TextParticleView particle={particle} />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
Reference in New Issue
Block a user