Last updated: May 29, 2025

<span aria-hidden="true" id="extract-thumbnails-from-videos-in-browsers-with-ffmpegwasm"></span>

# Extract thumbnails from videos in browsers with ffmpeg.wasm

![Kevin van Zonneveld](/assets/images/teammates/avatar-kvz-4.jpg?dpl=dpl_5eQDcFwLGbzg3PfkSEW2a2j2x5pY)

**Kevin van Zonneveld**

Co-founder · Amsterdam, The Netherlands · Show bio

[](https://x.com/kvz)[](https://github.com/kvz)

Video thumbnails are essential for modern web applications—they give users a quick visual preview and help them decide whether to engage with your content. With the rise of powerful WebAssembly (Wasm) tooling, you can now create those thumbnails entirely in the browser, keeping users’ media local while reducing server load.

<span aria-hidden="true" id="why-extract-thumbnails-in-the-browser"></span>

## Why extract thumbnails in the browser?

Extracting thumbnails client-side offers several advantages:

* **Reduced server load** – Encoding work is performed on the user’s device, freeing up back-end resources.
* **Immediate feedback** – Users see results instantly without a round-trip to your server.
* **Improved privacy** – Videos never leave the browser, which is especially helpful for sensitive content or when complying with privacy regulations.

<span aria-hidden="true" id="meet-ffmpegwasm"></span>

## Meet ffmpeg.wasm

[FFmpeg.wasm⁠](https://github.com/ffmpegwasm/ffmpeg.wasm) is a WebAssembly port of the popular FFmpeg toolkit. It exposes a familiar command-line-like API in JavaScript and runs entirely inside modern browsers.

Key features:

* Builds on FFmpeg 6, so most filters and codecs you know still work.
* Multithreaded thanks to WebAssembly threads.
* MIT-licensed and actively maintained.

<span aria-hidden="true" id="install-and-initialize"></span>

## Install and initialize

```bash
npm install @ffmpeg/ffmpeg@0.12.15 @ffmpeg/util

```

```javascript
// app.ts (or .js)
import { FFmpeg } from '@ffmpeg/ffmpeg'
import { fetchFile } from '@ffmpeg/util'

const ffmpeg = new FFmpeg()

export async function loadFFmpeg() {
  if (ffmpeg.loaded) return
  await ffmpeg.load()
}

```

Call `loadFFmpeg()` once—preferably lazily when the user opens an ‘Upload’ dialog—to avoid adding 25 MiB of Wasm to the initial bundle.

<span aria-hidden="true" id="satisfy-browser-requirements"></span>

## Satisfy browser requirements

FFmpeg.wasm requires the following browser features for optimal performance and functionality:

* **SharedArrayBuffer**: Essential for multithreading.
* **Cross-Origin Isolation**: Needed to enable `SharedArrayBuffer`.
* **WebAssembly**: The core technology FFmpeg.wasm is built upon.
* **WebAssembly Threads**: For parallel processing capabilities.

To enable these, your server must send the following HTTP headers for any page using FFmpeg.wasm:

```http
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

```

If you are using a service worker or your own CDN, make sure these headers are correctly propagated through every hop.

<span aria-hidden="true" id="extract-a-single-thumbnail"></span>

## Extract a single thumbnail

```javascript
export async function extractThumbnail(videoFile, time = '00:00:01') {
  await loadFFmpeg()

  // Write the file into FFmpeg’s virtual FS
  await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile))

  // Run the command: seek, grab one frame, output JPEG
  await ffmpeg.exec([
    '-i',
    'input.mp4',
    '-ss',
    time, // seek to <time>
    '-frames:v',
    '1',
    'thumb.jpg',
  ])

  // Read the result
  const data = await ffmpeg.readFile('thumb.jpg')
  return new Blob([data], { type: 'image/jpeg' })
}

```

Usage example with error handling:

```javascript
try {
  const blob = await extractThumbnail(file, '00:00:05')
  const url = URL.createObjectURL(blob)
  thumbnailImg.src = url
} catch (err) {
  console.error('Thumbnail extraction failed', err)
  if (err.message && err.message.includes('load() failed')) {
    console.error(
      'FFmpeg failed to load. This might be due to browser compatibility or network issues.',
    )
  } else if (err.message && err.message.includes('SharedArrayBuffer')) {
    console.error(
      'SharedArrayBuffer is not available. Ensure Cross-Origin Isolation headers are set.',
    )
  }
}

```

<span aria-hidden="true" id="grab-multiple-thumbnails-in-parallel"></span>

## Grab multiple thumbnails in parallel

You can reuse the same FFmpeg instance to extract several frames without re-loading the Wasm binary:

```javascript
export async function extractThumbnails(videoFile, marks = ['00:00:01', '00:00:05']) {
  await loadFFmpeg()
  await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile))

  for (const [idx, mark] of marks.entries()) {
    await ffmpeg.exec(['-i', 'input.mp4', '-ss', mark, '-frames:v', '1', `thumb_${idx}.jpg`])
  }

  return Promise.all(
    marks.map((_, idx) =>
      ffmpeg.readFile(`thumb_${idx}.jpg`).then((data) => new Blob([data], { type: 'image/jpeg' })),
    ),
  )
}

```

<span aria-hidden="true" id="keep-the-ui-responsive-with-a-web-worker"></span>

## Keep the UI responsive with a web worker

Running FFmpeg on the main thread blocks rendering. Offload the work to a dedicated worker:

```javascript
// worker.js
import { FFmpeg } from '@ffmpeg/ffmpeg'
import { fetchFile } from '@ffmpeg/util'

const ffmpeg = new FFmpeg()
self.onmessage = async ({ data }) => {
  const { file, time } = data

  try {
    if (!ffmpeg.loaded) await ffmpeg.load()
    await ffmpeg.writeFile('in.mp4', await fetchFile(file))
    await ffmpeg.exec(['-i', 'in.mp4', '-ss', time, '-frames:v', '1', 'out.jpg'])
    const img = await ffmpeg.readFile('out.jpg')
    self.postMessage({ ok: true, img })
  } catch (e) {
    self.postMessage({ ok: false, message: e.message })
  }
}

```

<span aria-hidden="true" id="performance-tips"></span>

## Performance tips

* **Service-worker cache** – Cache `ffmpeg-core.wasm` (around 25MB) so repeat visits load instantly.
* **Lazy loading** – Import the library and call `loadFFmpeg()` only when the user interacts with a feature that requires it, like selecting a video file.
* **Limit file size** – Large 4K videos may exceed browser memory or take too long to process. Consider capping uploads to a reasonable size, for example, 200 MB.
* **Reuse one instance** – Creating multiple FFmpeg instances wastes memory and slows down processing.
* **Fallback to server** – On incompatible browsers (e.g., those without `SharedArrayBuffer`support) or for very large files, enqueue the video for server-side processing instead.

<span aria-hidden="true" id="browser-versus-server-processing"></span>

## Browser versus server processing

|Aspect|Browser (FFmpeg.wasm)|Server (e.g., Transloadit)|
|-|-|-|
|Latency|Instant for the user’s own video|Round-trip + queue time|
|Privacy|Media never leaves the device|Needs upload and storage|
|Scalability|Limited by user hardware|Virtually unlimited|
|Implementation effort|JS library + headers|API call|
|Mobile battery usage|High|Low|
|Compatibility|Modern browsers with specific features|Universal|

Use whichever model best fits your product—or combine both for a robust solution.

<span aria-hidden="true" id="troubleshoot-common-issues"></span>

## Troubleshoot common issues

* **`SharedArrayBuffer` is unavailable** – Double-check your`Cross-Origin-Embedder-Policy: require-corp` and `Cross-Origin-Opener-Policy: same-origin`headers. Ensure they are correctly applied to the page serving FFmpeg.wasm.
* **`RangeError: Out of memory`** – The video might be too large or complex for the browser's available memory. Try trimming the video or downscaling it before processing with FFmpeg.wasm, or fall back to server-side processing.
* **Slow first run** – The initial download and compilation of `ffmpeg-core.wasm` can take time. Implement service worker caching and consider a "warm-up" call to `loadFFmpeg()` when the page becomes visible or idle, rather than waiting for direct user interaction.
* **Safari on iOS** – As of iOS 17, Safari does not support `SharedArrayBuffer` in Web Workers, which is crucial for FFmpeg.wasm's multithreading. Provide a server-side fallback for these users.

<span aria-hidden="true" id="streamline-production-with-transloadit"></span>

## Streamline production with Transloadit

When your app needs to process tens of thousands of videos a day—or must support every browser—our[🤖 /video/thumbs Robot](/docs/robots/video-thumbs.md) handles thumbnail extraction for you. It supports parallel thumbnail extraction, can generate a customizable count of 1-999 thumbnails per video, allows custom timestamps (using percentage or seconds), offers multiple output formats (JPEG, JPG, PNG), and includes advanced resize strategies such as crop, fit, fillcrop, min\_fit, pad, and stretch. Paired with our [Video Encoding service](/services/video-encoding.md), it scales automatically and never blocks the UI.

<span aria-hidden="true" id="next-steps"></span>

## Next steps

Experiment with FFmpeg.wasm locally, cache the Wasm core for a snappy UX, and decide where the browser-versus-server trade-off makes sense for your project. If you outgrow client-side limits, anAssembly that uses the `/video/thumbs` Robot is just one API call away.

[#devtips](/devtips.md)#video-thumbnail-extraction#browser-based-video-processing#ffmpeg-wasm-thumbnails#open-source-video-tools#web-applications-media-handling#video-encoding-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed

Cancel anytime
