import { useLingui } from "@lingui/react/macro"; import { buttonVariants } from "@rakazo/ui-web"; import { Download } from "lucide-react"; import type { RefObject } from "react"; import { useEffect, useRef, useState } from "react"; import { artifactUrl } from "../../lib/artifact-url"; export type MediaPlayerProps = { artifactId: string; kind: "video" | "audio"; name: string; posterArtifactId?: string; width?: number; height?: number; }; const MAX_VIDEO_WIDTH = 420; const MAX_VIDEO_HEIGHT = 480; /** Plays a video or audio artifact in place; loads nothing until it is near the viewport. */ export function MediaPlayer({ artifactId, kind, name, posterArtifactId, width, height, }: MediaPlayerProps) { const container = useRef(null); const near = useNearViewport(container); const [failed, setFailed] = useState(false); const [frame, setFrame] = useState<{ width: number; height: number } | null>(null); const src = near ? artifactUrl(artifactId) : undefined; if (failed) return ; if (kind === "audio") { return (
{/* biome-ignore lint/a11y/useMediaCaption: user media has no caption track. */}
); } // Uploads carry no dimensions; size the frame from the file once its metadata loads. const box = videoBox(width ?? frame?.width, height ?? frame?.height); return (
{/* biome-ignore lint/a11y/useMediaCaption: user media has no caption track. */}
); } /** Fits the frame into the chat column, defaulting to 16:9 until the size is known. */ function videoBox(width?: number, height?: number) { const w = width && width > 0 ? width : 16; const h = height && height > 0 ? height : 9; const scale = Math.min(MAX_VIDEO_WIDTH / w, MAX_VIDEO_HEIGHT / h); return { width: Math.round(w * scale), height: Math.round(h * scale) }; } function MediaFallback({ artifactId, name }: { artifactId: string; name: string }) { const { t } = useLingui(); return (
{name}
); } function useNearViewport(ref: RefObject) { const [near, setNear] = useState(false); useEffect(() => { const element = ref.current; if (!element || typeof IntersectionObserver === "undefined") { setNear(true); return; } const observer = new IntersectionObserver( (entries) => { if (entries.some((entry) => entry.isIntersecting)) { setNear(true); observer.disconnect(); } }, { rootMargin: "320px" }, ); observer.observe(element); return () => observer.disconnect(); }, [ref]); return near; }