Video Thumbnails
Extract video frame previews client-side without server processing
Overview
Videos can't be previewed as images, so Clawdy provides extractVideoThumbnail — a client-side utility that captures a single frame from a video file using the HTML5 Canvas API. No ffmpeg, no server processing, no extra dependencies.
The extracted frame is returned as a File that you can upload to Clawdy like any other image.
import { extractVideoThumbnail, isVideoFile, uploadFiles } from "@clawdyup/core/client";
// 1. Extract a frame
const thumbnail = await extractVideoThumbnail(videoFile);
// 2. Upload it
const [thumbResult] = await uploadFiles({
endpoint: "imageUploader",
files: [thumbnail],
});
console.log("Thumbnail URL:", thumbResult.url);extractVideoThumbnail runs entirely in the browser. It works with any video format the browser can play (MP4, WebM, MOV on Safari, etc.).
Usage with useClawdy Hook
The hook gives you full control over the upload flow, making it easy to extract a thumbnail and upload both files.
import { useState } from "react";
import { uploadFiles } from "@clawdyup/core/client";
import {
generateReactHelpers,
extractVideoThumbnail,
isVideoFile,
} from "@clawdyup/react";
const { useClawdy } = generateReactHelpers<OurFileRouter>();
function VideoUploader() {
const [thumbnailUrl, setThumbnailUrl] = useState<string>();
const { startUpload, isUploading } = useClawdy("videoUploader", {
onUploadProgress: (pct) => console.log(`Video: ${pct}%`),
});
const handleUpload = async (file: File) => {
if (!isVideoFile(file)) return;
// Extract thumbnail before uploading
try {
const thumb = await extractVideoThumbnail(file);
const [thumbResult] = await uploadFiles({
endpoint: "imageUploader",
files: [thumb],
});
setThumbnailUrl(thumbResult.url);
} catch (err) {
console.warn("Thumbnail extraction failed:", err);
}
// Upload the video
await startUpload([file]);
};
return (
<div>
<input
type="file"
accept="video/*"
onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
/>
{thumbnailUrl && <img src={thumbnailUrl} alt="Video preview" />}
</div>
);
}Usage with uploadFiles (Vanilla)
If you're not using React, use uploadFiles directly for both the thumbnail and video:
import {
extractVideoThumbnail,
isVideoFile,
uploadFiles,
} from "@clawdyup/core/client";
async function uploadVideo(file: File) {
let thumbnailUrl: string | undefined;
if (isVideoFile(file)) {
const thumb = await extractVideoThumbnail(file, {
seekTo: 2,
maxWidth: 640,
quality: 0.85,
format: "image/webp",
});
const [thumbResult] = await uploadFiles({
endpoint: "imageUploader",
files: [thumb],
});
thumbnailUrl = thumbResult.url;
}
const [videoResult] = await uploadFiles({
endpoint: "videoUploader",
files: [file],
onProgress: (pct) => console.log(`Uploading: ${pct}%`),
});
return {
videoUrl: videoResult.url,
thumbnailUrl,
};
}Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
seekTo | number | 1 | Time in seconds to capture the frame. Automatically clamped for short videos. |
maxWidth | number | 480 | Maximum pixel width. Height scales proportionally. |
quality | number | 0.8 | Image quality (0–1). |
format | string | "image/webp" | Output format: "image/webp", "image/jpeg", or "image/png". |
The output filename is derived from the original: my-video.mov produces my-video_thumb.webp.
Displaying Thumbnails
Use the thumbnail URL as the src for an <img> tag. Add a play icon overlay so users know it's a video:
function VideoCard({ videoUrl, thumbnailUrl, name }: {
videoUrl: string;
thumbnailUrl?: string;
name: string;
}) {
return (
<a href={videoUrl} target="_blank" rel="noopener noreferrer" className="relative block">
{thumbnailUrl ? (
<img src={thumbnailUrl} alt={name} className="aspect-video w-full object-cover rounded-lg" />
) : (
<div className="flex aspect-video w-full items-center justify-center rounded-lg bg-gray-100">
<span className="text-gray-400">No preview</span>
</div>
)}
{/* Play icon overlay */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="rounded-full bg-black/50 p-3">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
<polygon points="8,5 19,12 8,19" />
</svg>
</div>
</div>
</a>
);
}Browser Compatibility
Video codec support varies by browser. The extractVideoThumbnail function works with any format the browser can decode.
| Format | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| MP4 (H.264) | Yes | Yes | Yes | Yes |
| WebM (VP9) | Yes | Yes | No | Yes |
| MOV (H.264) | Yes | Partial | Yes | Yes |
| MOV (HEVC) | No | No | Yes | No |
If the browser can't decode the video, extractVideoThumbnail throws an error with the message "Failed to load video. The format may not be supported by this browser." — handle this gracefully with a try/catch.
Edge Cases
Short videos (< 1 second): The seekTo time is automatically clamped to 50% of the video duration, so a 0.5s video captures the frame at 0.25s.
Large videos: Only the video metadata and a single decoded frame are loaded into memory. URL.createObjectURL streams the file — even multi-GB videos produce small thumbnails without memory issues.
SSR / Server Components: extractVideoThumbnail is browser-only. Calling it in a server environment throws: "extractVideoThumbnail is only available in the browser". Always use it in client components or behind typeof window !== "undefined" checks.
WebP fallback: If the browser doesn't support WebP encoding (older Safari), the function automatically falls back to JPEG.