Video

How to Generate Video Thumbnails Programmatically: Node.js, FFmpeg, and Browser Methods

Ryan Trann

December 25, 2025

5 min read

Film reel representing video thumbnail generation

Thumbnails improve engagement and UX, help build feeds and preview grids, and provide fallbacks when video processing isn't done. If your product accepts video, you're going to need a way to generate thumbnails.

There are a few options to generate thumbnails. This guide shows the practical paths that actually work, including Node.js with FFmpeg, worker based generation, and browser side extraction.

The First Constraint: Where Are You Generating Them?

This guide will focus on how to generate video thumbnails in Node.js with FFmpeg. Video decoding requires a real runtime and the most ubiquitous option FFmpeg doesn't run everywhere.

  • Node backend (Next.js API routes, NestJS, Express): Works great.
  • Serverless functions (Lambda, Vercel, Netlify): Works, but setup is more tricky and resources are limited.
  • Edge runtimes (Cloudflare Workers): Cannot run FFmpeg at all.

The examples below target Node capable environments.

Approach 1: FFmpeg in Node Using ffmpeg-static + fluent-ffmpeg

ffmpeg-static bundles FFmpeg as an npm package providing binaries that work on different architectures (darwin, linux, win32). fluent-ffmpeg is a thin JavaScript wrapper that builds and runs FFmpeg commands without shell scripting (FFmpeg is originally a CLI tool).

Together, they let you generate thumbnails in pure Node environments (Next.js, NestJS, workers etc) with minimal setup.

Install

Install dependencies

bash
npm install ffmpeg-static fluent-ffmpeg

TypeScript Example

Generate thumbnail from local file

tsx
import ffmpeg from 'fluent-ffmpeg';
import ffmpegPath from 'ffmpeg-static';
import path from 'path';
import fs from 'fs/promises';

export async function generateThumbnail(inputPath: string, outputDir: string) {
  await fs.mkdir(outputDir, { recursive: true });
  const outputPath = path.join(outputDir, 'thumb.jpg');

  return new Promise<string>((resolve, reject) => {
    ffmpeg(inputPath)
      .setFfmpegPath(ffmpegPath!)
      .on('end', () => resolve(outputPath))
      .on('error', reject)
      .screenshots({
        timestamps: ["1"],
        filename: 'thumb.jpg',
        folder: outputDir,
      });
  });
}

Notes:

  • timestamps: You can enter one or more second value(s) or percentage(s) for determining where the thumbnail is taken from. Just make sure there's actually content at that time or you'll get a blank image.
  • size: Omitting the size param creates the thumbnail at the original video size, or you can specify a new size like 640x? which retains the original aspect ratio.

Common issues: Timestamp errors, corrupted metadata, or no tmp space in serverless functions.

Approach 2: Worker Based Generation Using Stored Video URLs

If you're storing videos in a Storage Bucket (S3, Supabase, Firebase, etc), you ideally don't want to process them inside the API call server side when uploading. Instead, run thumbnail generation asynchronously in a worker after the upload completes. That worker can be a separate service, a containerized job, or a background process. Anything that can fetch the video, run FFmpeg, and upload the thumbnail.

Step 1. User Uploads Video → You Store Metadata

Video metadata record

tsx
{
  id: 'video_123',
  originalUrl: 'https://s3.../original.mp4',
  status: 'processing'
}

Step 2. Worker Fetches the File

Fetch video from storage

tsx
const res = await fetch(originalUrl);
const arrayBuffer = await res.arrayBuffer();
// Write to /tmp or stream directly

Step 3. Generate Thumbnail

FFmpeg with raw options

tsx
ffmpeg()
  .input(stream)
  .inputOptions(['-ss 00:00:01'])
  .setFfmpegPath(ffmpegPath)
  .outputOptions(['-frames:v 1'])
  .output('thumb.jpg')
  .run();

What the flags do:

  • -ss 00:00:01 tells FFmpeg to seek to the 1-second mark before decoding. This avoids a potential blank frame that may exist at the start. You can adjust to wherever your video starts.
  • -frames:v 1 instructs FFmpeg to decode exactly one frame and stop.

Why this method instead of .screenshots() that we used in approach 1?

  • .screenshots() assumes the video is a seekable local file.
  • With streams from storage buckets (S3, GCS, Supabase, Firebase) seeking isn't reliable.
  • Raw FFmpeg (inputOptions + outputOptions) avoids temp file creation and is more predictable in worker environments.

Step 4. Upload Thumbnail Back to Storage

Upload to S3

tsx
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import fs from 'fs/promises';

const s3 = new S3Client({ region: 'us-west-2' });

await s3.send(
  new PutObjectCommand({
    Bucket: 'my-bucket',
    Key: `videos/${id}/thumb.jpg`,
    Body: await fs.readFile('thumb.jpg'),
    ContentType: 'image/jpeg'
  })
);

Approach 3: Client Side Thumbnail Extraction (Browser)

Useful for client UX that requires preview images before upload.

Browser-side thumbnail extraction

tsx
export async function extractThumbnail(file: File): Promise<Blob> {
  const video = document.createElement('video');
  video.src = URL.createObjectURL(file);
  video.preload = 'metadata';

  await new Promise(res => video.onloadedmetadata = res);

  video.currentTime = video.duration / 2;
  await new Promise(res => video.onseeked = res);

  const canvas = document.createElement('canvas');
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  const ctx = canvas.getContext('2d')!;
  ctx.drawImage(video, 0, 0);

  return new Promise(resolve => 
    canvas.toBlob(blob => resolve(blob!), 'image/jpeg', 0.90)
  );
}

Pros: Instant UX, zero backend load.

Cons: Browser memory limits, inconsistencies, not usable on workers.

Which Method to Choose

  • If you control a Node backend: Use FFmpeg via ffmpeg-static to generate video thumbnails server-side.
  • If you run a job/worker system: Download to worker → generate → upload to storage bucket.
  • If you want instant upload previews: Do client-side extraction.

Some apps use both client side and server side thumbnail generation to provide the best UX.

Debugging Video Thumbnail Generation

  • Black thumbnails usually mean a bad timestamp.
  • Mobile uploads can lie about duration.
  • "Invalid data found" usually means the file hasn't finished uploading.
  • Never generate thumbnails directly inside an API request; offload it.

If you'd rather skip the setup entirely, Hyperserve can generate thumbnails automatically on every upload. You don't have to setup workers, Lambda layers or FFmpeg. We handle the architecture complexity, just upload a video to our API and get a thumbnail URL back. That's why we built Hyperserve—to make adding video to your app as simple as possible.

Want to add video to your app quickly?
Try Hyperserve!

Hyperserve

Video Hosting API made simple.

Product

FeaturesPricingBlog

© 2025 Hyperserve. All rights reserved.