Extract Frames and Thumbnails
Grab a still frame at any timestamp to build thumbnails and preview images.
Follow along with a live API key. 100 free minutes, no credit card.
Get free API keyA thumbnail is just a single frame
A thumbnail or preview image is one still frame pulled out of a video at a specific moment. You pick a timestamp, tell the API to emit exactly one frame, and choose an image outputFormat. The job runs the same way every transcode does. The only difference is that the output is an image instead of a video.
You already have createTranscode, waitForJob, and getDownloadUrl from Lesson 3. This lesson just changes the request body you pass to createTranscode.
The request body
To grab a single JPEG frame at the two-second mark, send an image outputFormat and three options:
{
inputs: [{ url }],
outputFormat: 'jpeg',
options: [
{ option: '-ss', argument: '00:00:02' },
{ option: '-frames:v', argument: '1' },
{ option: '-q:v', argument: '2' },
],
}outputFormatacceptsjpeg,png,gif, andwebpfor stills. Use'jpeg', not'jpg'.-ssseeks to the timestamp. It accepts'HH:MM:SS'or plain seconds like'2'.-frames:vsets how many frames to output. Use'1'for a single still.-q:vsets image quality, where lower is higher quality.'2'is high quality for JPEG.
Do not use -vframes. The FFmpeg CLI habit is to write -vframes 1, but the API rejects it with 400 "Invalid FFmpeg options". Use -frames:v instead. It does the same thing and is the accepted form.
An extractFrame helper
Wrap the body in a small helper so you can pass a timestamp and an optional format. It calls the same createTranscode you built in Lesson 3.
// extract-frame.mjs
async function extractFrame(url, timestamp = '00:00:02', { format = 'jpeg' } = {}) {
return createTranscode({
inputs: [{ url }],
outputFormat: format,
options: [
{ option: '-ss', argument: timestamp },
{ option: '-frames:v', argument: '1' },
{ option: '-q:v', argument: '2' },
],
})
}A PNG variant
JPEG is small and fine for photographic frames. When you need a lossless image, say a frame with sharp text or UI, pass png instead. The -q:v option is a JPEG quality control, so it simply has no effect here, but leaving it in is harmless.
// Same helper, PNG output:
const job = await extractFrame(videoUrl, '00:00:05', { format: 'png' })
// Or spell out the body directly:
await createTranscode({
inputs: [{ url: videoUrl }],
outputFormat: 'png',
options: [
{ option: '-ss', argument: '00:00:05' },
{ option: '-frames:v', argument: '1' },
],
})Putting it together with the Lesson 3 flow
Extracting a frame is the full create, poll, and download lifecycle you already know. Submit the job with extractFrame, wait for it with waitForJob, get a signed URL with getDownloadUrl, then stream the bytes to disk.
// thumbnail.mjs
import { writeFile } from 'node:fs/promises'
const videoUrl = 'https://example.com/input.mp4'
// 1. Submit the frame-extraction job
const { id } = await extractFrame(videoUrl, '00:00:02', { format: 'jpeg' })
// 2. Poll until it finishes
await waitForJob(id)
// 3. Get a signed URL for the output image
const downloadUrl = await getDownloadUrl(id)
// 4. Download the image to disk
const res = await fetch(downloadUrl)
if (!res.ok) {
throw new Error(`Download failed: ${res.status}`)
}
const bytes = Buffer.from(await res.arrayBuffer())
await writeFile('thumbnail.jpeg', bytes)
console.log('Saved thumbnail.jpeg')Run it:
node --env-file=.env thumbnail.mjs
# Saved thumbnail.jpegWhat could go wrong: Passing -vframes returns a 400 "Invalid FFmpeg options". Switch to -frames:v. And a -ss value past the end of the video seeks to nothing, so you get no frame and a failed job. Check the duration first: the upload confirm response includes metadata.duration_seconds, or run a probe, and keep your timestamp inside it.
Key takeaways
- Set
outputFormattojpeg,png,gif, orwebpto emit a still image. - Seek to the moment you want with
-ss('HH:MM:SS'or seconds). - Emit a single frame with
-frames:vset to'1', never-vframes, which returns a 400. - Control JPEG quality with
-q:v, where lower means higher quality.
Next up: go beyond presets with custom FFmpeg options and filters to resize, crop, and overlay.
Build this into your app. No FFmpeg install
One call kicks off a job. No local FFmpeg, no servers, no worker queue to run.
const res = await fetch('https://api.ffmpeg-micro.com/v1/transcodes', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: [{ url: 'gs://your-bucket/input.mp4' }],
outputFormat: 'mp4',
preset: { quality: 'medium', resolution: '1080p' },
}),
})
const job = await res.json() // { id, status: 'pending' }