improve stream previews

This commit is contained in:
talksik
2026-02-21 13:21:27 -08:00
parent cbb3f50a25
commit 0fcec25a26
6 changed files with 408 additions and 127 deletions
@@ -18,6 +18,22 @@ function formatTime(ms: number): string {
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
function DurationPill({
currentTimeMs,
totalDurationMs,
}: {
currentTimeMs: number;
totalDurationMs: number;
}) {
return (
<div className="absolute top-3 right-3 rounded-full bg-white/10 px-2.5 py-1 backdrop-blur-sm">
<span className="font-mono text-xs text-white/80">
{formatTime(currentTimeMs)} / {formatTime(totalDurationMs)}
</span>
</div>
);
}
export function MediaParticleView({
particle,
onEnded,
@@ -35,7 +51,10 @@ export function MediaParticleView({
const [audioEl, setAudioEl] = useState<HTMLAudioElement | null>(null);
const [currentTimeMs, setCurrentTimeMs] = useState(0);
const audioSource = useAudioSource(audioEl);
const data = particle.data as MediaParticleData;
const isAudio = data.mime_type?.startsWith("audio/");
const audioSource = useAudioSource(isAudio ? audioEl : null);
useEffect(() => {
if (cachedUrl) {
@@ -60,8 +79,19 @@ export function MediaParticleView({
};
}, [particle.id, cachedUrl, cacheDownloadUrl]);
// Start audio playback once the AudioContext source is ready
useEffect(() => {
const el = videoRef.current ?? audioRef.current;
const el = audioRef.current;
if (!el || !audioSource) return;
if (!paused) {
el.play().catch(() => {});
}
}, [audioSource, paused]);
// Handle video pause/resume
useEffect(() => {
const el = videoRef.current;
if (!el) return;
if (paused) {
@@ -71,6 +101,16 @@ export function MediaParticleView({
}
}, [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">
@@ -83,9 +123,6 @@ export function MediaParticleView({
return <Skeleton className="h-full w-full rounded-none" />;
}
const data = particle.data as MediaParticleData;
const isAudio = data.mime_type?.startsWith("audio/");
if (isAudio) {
return (
<div className="relative flex h-full w-full items-center justify-center bg-black/90">
@@ -94,8 +131,8 @@ export function MediaParticleView({
audioRef.current = el;
setAudioEl(el);
}}
crossOrigin="anonymous"
src={url}
autoPlay
onEnded={onEnded}
onTimeUpdate={(e) => {
setCurrentTimeMs(e.currentTarget.currentTime * 1000);
@@ -106,23 +143,31 @@ export function MediaParticleView({
<AudioLevelBars sourceNode={audioSource.sourceNode} />
)}
<div className="absolute top-3 right-3 rounded-full bg-black/30 px-2.5 py-1 backdrop-blur-sm">
<span className="font-mono text-xs text-white/80">
{formatTime(currentTimeMs)} / {formatTime(data.duration_ms)}
</span>
</div>
<DurationPill
currentTimeMs={currentTimeMs}
totalDurationMs={data.duration_ms}
/>
</div>
);
}
return (
<video
ref={videoRef}
src={url}
autoPlay
playsInline
onEnded={onEnded}
className="h-full w-full object-cover"
/>
<div className="relative h-full w-full">
<video
ref={videoRef}
src={url}
autoPlay
playsInline
onEnded={onEnded}
onTimeUpdate={(e) => {
setCurrentTimeMs(e.currentTarget.currentTime * 1000);
}}
className="h-full w-full object-cover"
/>
<DurationPill
currentTimeMs={currentTimeMs}
totalDurationMs={data.duration_ms}
/>
</div>
);
}