import { useCallback, useEffect, useRef, useState } from "react"; import { DownloadMediaButton } from "../components/DownloadMediaButton"; function formatTime(sec: number): string { if (!Number.isFinite(sec)) return "0:00"; const s = Math.floor(sec % 60); const m = Math.floor(sec / 60); return `${m}:${s.toString().padStart(2, "0")}`; } export function ToolsVideoFrame() { const videoRef = useRef(null); const canvasRef = useRef(null); const [file, setFile] = useState(null); const [url, setUrl] = useState(null); const [duration, setDuration] = useState(0); const [currentTime, setCurrentTime] = useState(0); const [previewUrl, setPreviewUrl] = useState(null); const [err, setErr] = useState(null); useEffect(() => { return () => { if (url) URL.revokeObjectURL(url); }; }, [url]); const drawFrame = useCallback(() => { const video = videoRef.current; const canvas = canvasRef.current; if (!video || !canvas || video.readyState < 2) return; const w = video.videoWidth; const h = video.videoHeight; if (w === 0 || h === 0) return; canvas.width = w; canvas.height = h; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.drawImage(video, 0, 0, w, h); setPreviewUrl(canvas.toDataURL("image/png")); }, []); useEffect(() => { const v = videoRef.current; if (!v || !url) return; const onMeta = () => { const d = v.duration; setDuration(Number.isFinite(d) ? d : 0); setCurrentTime(0); v.currentTime = 0; requestAnimationFrame(() => drawFrame()); }; const onSeeked = () => { setCurrentTime(v.currentTime); drawFrame(); }; const onTime = () => setCurrentTime(v.currentTime); v.addEventListener("loadedmetadata", onMeta); v.addEventListener("seeked", onSeeked); v.addEventListener("timeupdate", onTime); return () => { v.removeEventListener("loadedmetadata", onMeta); v.removeEventListener("seeked", onSeeked); v.removeEventListener("timeupdate", onTime); }; }, [url, drawFrame]); function onPickVideo(f: File | null) { setErr(null); setPreviewUrl(null); if (url) URL.revokeObjectURL(url); setUrl(null); setFile(f); if (!f) return; if (!f.type.startsWith("video/")) { setErr("请选择视频文件。"); return; } const u = URL.createObjectURL(f); setUrl(u); } function onScrub(t: number) { const v = videoRef.current; if (!v) return; v.currentTime = t; } return (

提取视频帧

本地选视频,拖动进度条选时刻,预览后下载 PNG。不上传服务器。

onPickVideo(e.target.files?.[0] ?? null)} /> {file && (

已选:{file.name}({(file.size / (1024 * 1024)).toFixed(2)} MB)

)}
{url && ( <>
{formatTime(currentTime)} / {formatTime(duration)}
onScrub(Number((e.target as HTMLInputElement).value))} onChange={(e) => onScrub(Number(e.target.value))} className="w-full accent-ink h-2 rounded-full appearance-none bg-slate-400/90" />

停稳后显示当前帧预览。

{previewUrl && (

当前帧预览

当前帧
)} )} {err &&

{err}

}
); }