import { useEffect, useRef, useState } from "react"; import type { MediaParticleData, StreamParticle } from "@/api/types"; import { apiClient } from "@/api/client"; import { usePlaybackStore } from "@/stores/playback-store"; import { Skeleton } from "@/components/ui/skeleton"; import { AudioLevelBars } from "@/components/audio/audio-level-bars"; import { useAudioSource } from "@/components/audio/use-audio-source"; interface MediaParticleViewProps { particle: StreamParticle; onEnded: () => void; } function formatTime(ms: number): string { const totalSeconds = Math.floor(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}:${seconds.toString().padStart(2, "0")}`; } function DurationPill({ currentTimeMs, totalDurationMs, }: { currentTimeMs: number; totalDurationMs: number; }) { return (
{formatTime(currentTimeMs)} / {formatTime(totalDurationMs)}
); } export function MediaParticleView({ particle, onEnded, }: MediaParticleViewProps) { const cachedUrl = usePlaybackStore( (s) => s.downloadUrlCache[particle.id], ); const cacheDownloadUrl = usePlaybackStore((s) => s.cacheDownloadUrl); const paused = usePlaybackStore((s) => s.paused); const [url, setUrl] = useState(cachedUrl ?? null); const [error, setError] = useState(null); const videoRef = useRef(null); const audioRef = useRef(null); const [audioEl, setAudioEl] = useState(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; } let cancelled = false; apiClient .getParticleDownloadUrl(particle.id) .then((downloadUrl) => { if (cancelled) return; cacheDownloadUrl(particle.id, downloadUrl); setUrl(downloadUrl); }) .catch(() => { if (!cancelled) setError("Failed to load media"); }); return () => { cancelled = true; }; }, [particle.id, cachedUrl, cacheDownloadUrl]); // Start audio playback once the AudioContext source is ready useEffect(() => { 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) { el.pause(); } else { el.play().catch(() => {}); } }, [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 (
{error}
); } if (!url) { return ; } if (isAudio) { return (
); } return (
); }