import { settlePromise } from "@t3tools/client-runtime/state/runtime"; import { AsyncResult } from "effect/reactivity"; import { PlayIcon, RotateCwIcon, TriangleAlertIcon } from "lucide-react"; import { useState, type CSSProperties } from "react"; import { observeIntersection } from "../../lib/observeIntersection"; 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 onOpen?: (() => void) | undefined; readonly className?: string | undefined; readonly videoClassName?: string | undefined; readonly stateClassName?: string | undefined; readonly style?: CSSProperties | undefined; readonly copyMarkdown?: string | undefined; readonly onRetry?: (() => Promise) | undefined; readonly actionsSource?: MediaActionSource | undefined; } export function MediaVideoPlayer({ src: latestSrc, label, sourceFailed = false, originalUrl, revision = null, preload = "visible", autoPlay = false, onOpen, className, videoClassName, stateClassName, style, copyMarkdown, onRetry, actionsSource, }: MediaVideoPlayerProps) { const [playbackSource, setPlaybackSource] = useState<{ src: string; revision: string | null; } | null>(null); const [playing, setPlaying] = useState(false); 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; const preloaded = preloadedSrc === src || typeof IntersectionObserver === "undefined"; // Re-signing must not reload a loaded source. Changed files load once playback pauses. if ( playbackSource !== null && playbackSource.revision !== revision && (latestSrc !== null || sourceFailed) && !playing ) { setPlaybackSource( revision !== null && latestSrc !== null ? { src: latestSrc, revision } : null, ); } // Retry and failure remount the element, so its listeners live and die with it. const attachVideo = (video: HTMLVideoElement) => { 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(); // A detached element's pause event never reaches onPause. setPlaying(false); }; }; // On the wrapper, not the video: re-running the video's ref callback would pause playback. const observePreload = (player: HTMLSpanElement) => { if (src === null || failed || preload === "metadata" || preloaded) return; return observeIntersection( player, (entry) => { if (entry.isIntersecting) setPreloadedSrc(src); }, { rootMargin: "200px" }, ); }; const retry = async () => { if (retrying) return; setRetrying(true); const result = await settlePromise(async () => { await onRetry?.(); }); if (AsyncResult.isSuccess(result)) { setPlaybackSource(null); setFailedSrc(null); setLoadAttempt((current) => current + 1); } else { setFailedSrc(src); } setRetrying(false); }; const player = ( {failed && !onOpen ? ( Video unavailable{label ? ` · ${label}` : ""} {latestSrc !== null || onRetry ? ( ) : null} ) : src !== null && !failed ? (