Clawdy
Guides

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

OptionTypeDefaultDescription
seekTonumber1Time in seconds to capture the frame. Automatically clamped for short videos.
maxWidthnumber480Maximum pixel width. Height scales proportionally.
qualitynumber0.8Image quality (0–1).
formatstring"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.

FormatChromeFirefoxSafariEdge
MP4 (H.264)YesYesYesYes
WebM (VP9)YesYesNoYes
MOV (H.264)YesPartialYesYes
MOV (HEVC)NoNoYesNo

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.

On this page