import { useEffect, useRef, useState } from "react"; import type { Particle } from "@/api/types"; import type { ParticlePath } from "@/lib/particle-path"; import { useDownloadUrl } from "@/hooks/use-download-url"; import { useTranscriptPlayback } from "@/hooks/use-transcript-playback"; import { TranscriptOverlay } from "@/features/particles/transcript-overlay"; import { Skeleton } from "@/components/ui/skeleton"; import { AudioLevelBars } from "@/components/audio/audio-level-bars"; import { useAudioSource } from "@/components/audio/use-audio-source"; import { useParticleAttachments } from "@/hooks/use-particle-attachments"; import { ParticleAttachments } from "@/features/particles/particle-attachments"; type MediaParticle = Extract; interface MediaParticleViewProps { particle: MediaParticle; streamPath: ParticlePath; paused: boolean; onEnded: () => void; onProgress?: (ratio: number) => void; } export function MediaParticleView({ particle, streamPath, paused, onEnded, onProgress, }: MediaParticleViewProps) { const { data: url, error } = useDownloadUrl(particle.properties.object_id); const { attachments } = useParticleAttachments(streamPath, particle.id); const videoRef = useRef(null); const audioRef = useRef(null); const isAudio = particle.properties.mime_type?.startsWith("audio/"); const [currentTime, setCurrentTime] = useState(0); const transcript = particle.properties.transcript; const { activeSentence, activeWordIndex } = useTranscriptPlayback( transcript, currentTime, ); // WORKAROUND: useAudioSource needs an audio element, but audioRef is only set after mount const [audioEl, setAudioEl] = useState(null); const audioSource = useAudioSource(audioEl); useEffect(() => { const el = isAudio ? audioRef.current : videoRef.current; if (!el) return; if (paused) { el.pause(); } else { el.play().catch(() => { console.warn("Playback failed", { particleId: particle.id }); }); } }, [paused, isAudio, particle.id]); if (error) { return (
Failed to load media
); } if (!url) { return ; } const handleTimeUpdate = (e: React.SyntheticEvent) => { const { currentTime: time, duration } = e.currentTarget; setCurrentTime(time); if (duration > 0) onProgress?.(time / duration); }; const attachmentOverlay = attachments.length > 0 && (
); if (isAudio) { return (
); } return (
); }