import { RotateCwIcon, TriangleAlertIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react"; import { cn } from "../../lib/utils"; import { prepareVideoFirstFrame } from "../../lib/videoFirstFrame"; import { Button } from "../ui/button"; import { OpenMediaLink } from "./OpenMediaLink"; import { MediaActions, type MediaActionSource } from "./MediaActions"; interface MediaVideoPlayerProps { readonly src: string | null; readonly label: string; readonly sourceFailed?: boolean | undefined; readonly originalUrl?: string | undefined; readonly revision?: string | null | undefined; readonly preload?: "visible" | "metadata" | undefined; readonly autoPlay?: boolean | undefined; readonly className?: string | undefined; readonly videoClassName?: string | undefined; /** Styles the loading and failure panels, which otherwise assume an inline light surface. */ readonly stateClassName?: string | undefined; readonly style?: CSSProperties | undefined; readonly copyMarkdown?: string | undefined; readonly onRetry?: (() => Promise) | undefined; readonly actionsSource?: MediaActionSource | undefined; } /** Keeps native range streaming and playback state consistent across inline and file previews. */ export function MediaVideoPlayer({ src: latestSrc, label, sourceFailed = false, originalUrl, revision = null, preload = "visible", autoPlay = false, className, videoClassName, stateClassName, style, copyMarkdown, onRetry, actionsSource, }: MediaVideoPlayerProps) { const videoRef = useRef(null); const [playbackSource, setPlaybackSource] = useState<{ src: string; revision: string | null; } | null>(null); const [failedSrc, setFailedSrc] = useState(null); const [retrying, setRetrying] = useState(false); const [loadAttempt, setLoadAttempt] = useState(0); const [preloadedSrc, setPreloadedSrc] = useState(null); const src = playbackSource?.src ?? latestSrc; const sourceRevision = playbackSource === null ? revision : playbackSource.revision; const failed = src !== null ? failedSrc === src : sourceFailed; // Re-signing must not reset the playhead. Changed files refresh once playback pauses. const refreshPausedRevision = useCallback(() => { const video = videoRef.current; if (video === null || video.paused || video.ended) { setPlaybackSource((current) => current !== null && current.revision !== revision ? null : current, ); } }, [revision]); useEffect(refreshPausedRevision, [refreshPausedRevision]); useEffect(() => { const video = videoRef.current; if (!video || preload === "metadata" || preloadedSrc === src) return; if (typeof IntersectionObserver === "undefined") { setPreloadedSrc(src); return; } let active = true; const observer = new IntersectionObserver( (entries) => { if (!active || !entries.some((entry) => entry.isIntersecting)) return; setPreloadedSrc(src); observer.disconnect(); }, { rootMargin: "200px" }, ); observer.observe(video); return () => { active = false; observer.disconnect(); }; }, [src, preload, preloadedSrc, failed, loadAttempt]); useEffect(() => { const video = videoRef.current; if (!video) return; const pauseWhenHidden = () => { // Native fullscreen can hide the inline page while this video is still visible. const fullscreen = document.fullscreenElement?.contains(video) || ("webkitDisplayingFullscreen" in video && video.webkitDisplayingFullscreen === true); if (document.hidden && !fullscreen) video.pause(); }; document.addEventListener("visibilitychange", pauseWhenHidden); document.addEventListener("fullscreenchange", pauseWhenHidden); video.addEventListener("webkitendfullscreen", pauseWhenHidden); return () => { document.removeEventListener("visibilitychange", pauseWhenHidden); document.removeEventListener("fullscreenchange", pauseWhenHidden); video.removeEventListener("webkitendfullscreen", pauseWhenHidden); video.pause(); }; }, [src, failed, loadAttempt]); const retry = async () => { if (retrying) return; setRetrying(true); try { await onRetry?.(); setPlaybackSource(null); setFailedSrc(null); setLoadAttempt((current) => current + 1); } catch { setFailedSrc(src); } finally { setRetrying(false); } }; const player = ( {failed ? ( Video unavailable{label ? ` · ${label}` : ""} {latestSrc !== null || onRetry ? ( ) : null} ) : src !== null ? (