# Video Frame Extractor — Video to Images, Scenes & Thumbnails (`nexgensignal/video-frame-extractor`) Actor

Turn video files into still images: auto storyboard, every N seconds, N evenly spaced, scene changes, keyframes or set times. Skips blank frames, makes contact sheets. MP4, MOV, WebM, MKV in; JPEG, PNG or WebP out. Part of NexGen Signal — data tools and official-source data products

- **URL**: https://apify.com/nexgensignal/video-frame-extractor.md
- **Developed by:** [NexGen Signal](https://apify.com/nexgensignal) (community)
- **Categories:** Videos, AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $33.50 / 1,000 frame extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 🎞️ Video Frame Extractor — Video to Images, Scenes & Thumbnails

Give direct links to **video files** (MP4, MOV, WebM, MKV, AVI and more) and get **still images taken from them**, one row per frame with its time in the video, its size and a download link to the image.

Choose how frames are picked: an **auto storyboard** (the start, the strongest scene change in each part of the video, topped up with evenly spaced frames), **every N seconds**, **a set number of evenly spaced frames**, **at every scene change** (each new shot in an edited video), **at every keyframe**, or **at times you list**. Blank frames (fades, black intros) are skipped for the next frame with content. Limit it to part of the video, shrink frames to a maximum width or height, save them as JPEG, PNG or WebP, and get a free **contact sheet** (a grid of the frames with their timecodes) per video. Every row also carries the video's length, size, frame rate and codec.

The unit of output is one frame delivered. Videos that cannot be read, and links that are not videos, are not billed.

### Who buys this and for what job

- **AI and computer-vision teams** turning video into image datasets or feeding frames to vision models.
- **Video editors and marketers** making thumbnails and storyboards.
- **Archivists and researchers** sampling long recordings shot by shot.
- **Developers** adding a frame-grab step to a video pipeline without running FFmpeg themselves.

Buyers usually search for this as **"video frame extractor"**, **"video to images"** or **"extract frames from video"**.

### 📊 Sample Output

![Table of storyboard frames: timecode, mode, scene score, width, height and bytes.](https://api.apify.com/v2/key-value-stores/IXCaMKjxSmUTLHhmq/records/video-frame-extractor.png)

| timecode | scene_score | width | height | bytes |
|---|---|---|---|---|
| 00:00:00.000 | — | 284 | 160 | 5613 |
| 00:00:13.333 | 0.5856 | 284 | 160 | 4881 |
| 00:00:30.267 | 0.6225 | 284 | 160 | 6742 |
| 00:00:41.467 | 0.496 | 284 | 160 | 7231 |
| 00:01:09.667 | 0.6121 | 284 | 160 | 7265 |
| 00:01:35.667 | 0.7648 | 284 | 160 | 7569 |
| 00:01:52.800 | 0.6952 | 284 | 160 | 5686 |

Rows from live run `4Xraff0Efi2LUM3gZ` on Apify (auto mode, 12 frames and a contact sheet from the 320 × 180 copy of a 3.5-minute NASA SLS resource reel, frames shrunk to 160 px high). Notice that the first frame has no scene score (the start is always included) and the cuts are spread across the whole reel, not bunched where the editing is fastest.

### ✅ What you get

Every field of each record:

- `record_id`, `video_url`, `frame_index` — id from the link and time, the video link, and the frame's place in the plan.
- `timestamp_seconds`, `timecode`, `mode`, `scene_score` — when the frame is in the video, how it was chosen, and (auto and scene modes) how different it is from the frame before, 0 to 1.
- `planned_timestamp_seconds` — set when a blank frame was skipped: the time first planned.
- `contact_sheet_url` — the video's contact sheet (with contactSheet).
- `width`, `height`, `format`, `bytes`, `sha256` — the image.
- `file_key`, `file_url` — the saved image in the run's key-value store (empty when saveFrames is off).
- `video_duration_seconds`, `video_width`, `video_height`, `video_fps`, `video_codec` — the video itself.
- `extracted_at` — when the frame was taken (UTC).

### ⚙️ Sample inputs

**Auto storyboard with a contact sheet (the Store example)**

```json
{
  "videoUrls": ["https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4"],
  "mode": "auto",
  "contactSheet": true,
  "maxFrames": 10
}
```

The 1969 Apollo 11 launch film: 10 frames that cover it, plus a grid image.

**A frame every 10 seconds**

```json
{
  "videoUrls": ["https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4"],
  "mode": "interval",
  "intervalSeconds": 10,
  "maxFrames": 100
}
```

9 frames from its 84 seconds.

**Every scene change**

```json
{
  "videoUrls": ["https://images-assets.nasa.gov/video/April%202020%20SLS%20Resource%20Reel%20Short%20Form%20(V1)/April%202020%20SLS%20Resource%20Reel%20Short%20Form%20(V1)~mobile.mp4"],
  "mode": "scenes",
  "maxWidth": 640,
  "maxFrames": 100
}
```

One frame per new shot.

**Five thumbnails from part of a video, as WebP**

```json
{
  "videoUrls": ["https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4"],
  "mode": "count",
  "frameCount": 5,
  "startSeconds": 20,
  "endSeconds": 70,
  "imageFormat": "webp",
  "maxWidth": 320,
  "maxFrames": 10
}
```

Evenly spaced between 20 s and 70 s.

Input fields:

| Field | Meaning |
|---|---|
| `videoUrls` | direct video file links (up to 500). Not YouTube or social media pages. |
| `mode` | auto, interval, count, scenes, keyframes or timestamps. |
| `intervalSeconds`, `frameCount`, `timestamps` | the settings for those modes (timestamps as 12.5, 00:01:30 or 1m30s). |
| `skipBlankFrames`, `contactSheet` | skip black or flat frames (default on); save a grid of the frames per video (default off). |
| `maxHeight` | shrink to this height (0 = full size). |
| `sceneThreshold`, `sceneMinGapSeconds` | scene mode: sensitivity (default 0.3) and the minimum gap between cuts (default 1 s). |
| `startSeconds`, `endSeconds` | only take frames in this part of the video. |
| `imageFormat`, `quality`, `maxWidth` | jpg, png or webp; quality 1–100; shrink to this width (0 = full size). |
| `saveFrames` | store images with links (default on). |
| `maxFramesPerVideo`, `maxVideoMb` | per-video limits (defaults 500 frames, 1,000 MB). |
| `maxFrames` | stop after this many frames are delivered (default 1,000). Each delivered frame is billed. |

### 🧾 JSON sample record

```json
{
  "record_id": "d635af3c248c641416851395",
  "video_url": "https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4",
  "frame_index": 1,
  "timestamp_seconds": 2.7,
  "timecode": "00:00:02.700",
  "mode": "auto",
  "scene_score": 1,
  "planned_timestamp_seconds": 1.2,
  "width": 480,
  "height": 480,
  "format": "jpg",
  "bytes": 16905,
  "sha256": "f39aa53935def09d25077ebb9b0f5a06caed5bc3169d622b9e5fe9c276d3bd75",
  "file_key": "frame-a9a6a5a99d1d-0000002700.jpg",
  "file_url": "https://api.apify.com/v2/key-value-stores/X0Hrgi4NA6m0eOw7i/records/frame-a9a6a5a99d1d-0000002700.jpg",
  "contact_sheet_url": "https://api.apify.com/v2/key-value-stores/X0Hrgi4NA6m0eOw7i/records/sheet-a9a6a5a99d1d.jpg",
  "video_duration_seconds": 84.2,
  "video_width": 480,
  "video_height": 480,
  "video_fps": 30,
  "video_codec": "h264",
  "extracted_at": "2026-10-18T00:06:23Z"
}
```

### 🔧 How it works

Each video is downloaded once and read with FFmpeg. For 'every N seconds' and 'evenly spaced' modes all frames are written in one pass through the video (each frame is the source frame nearest its time, so within one source frame); for listed times each frame is taken with an accurate seek. Scene mode reads the whole video at low resolution, scores how much each frame differs from the one before, keeps cuts above your threshold (merging cuts closer than the minimum gap and keeping the strongest), then takes those frames at full size. Keyframe mode lists the video's keyframes. Frames are taken a few at a time in parallel and delivered in time order.

Every run reads each host's robots.txt first: a path it disallows is skipped, listed in the run receipt and never charged. Only public internet addresses are fetched (no private, loopback or cloud-metadata addresses, checked again on every redirect). No logins, no CAPTCHA solving, no proxies.

Saved files go to the run's own key-value store. If your Apify account keeps storages private, add your API token to the link (`?token=…`) or open the store from the run in Apify Console.

Delivery is pushed first and charged after. The run receipt (`RUN_RECEIPT`) lists each video with its length, size, frame rate, codec and frames delivered, and the delivered and charged counts.

### 💰 Pricing example

Per frame delivered, no start fee, no monthly charge:

| Apify plan | Price per frame |
|---|---|
| Free | $0.05 |
| Bronze | $0.045 |
| Silver | $0.04 |
| Gold, Platinum, Diamond | $0.0335 |

- 10 frames: $0.50 on the Free plan, $0.34 on Gold.
- 1,000 frames: $50.00 on the Free plan, $33.50 on Gold.
- 10,000 frames: $500.00 on the Free plan, $335.00 on Gold.

**Coverage and size.** Proof runs on 17 Oct 2026: 40 frames every 2 seconds from an 84-second NASA film in about 14 seconds (one FFmpeg pass); a 12-frame auto storyboard with contact sheet from a 3.5-minute NASA reel in about 7 seconds; 30 scene-change frames from the same reel in about 11 seconds. Runs use 4 GB of memory (one CPU core) by default; long videos in scene mode take longer, and more memory gives more cores.

### ⚖️ Legal & ToS

Videos belong to their owners. You choose the links. Make sure you have the right to download and use the content you point it at; the actor fetches only what you give it (and, where you turn it on, what those pages link to).

Every run reads each host's robots.txt first: a path it disallows is skipped, listed in the run receipt and never charged. Only public internet addresses are fetched (no private, loopback or cloud-metadata addresses, checked again on every redirect). No logins, no CAPTCHA solving, no proxies.

### ❓ FAQ

**Can I give a YouTube link?**
No. Give a direct link to the video file.

**Which formats work?**
Anything FFmpeg reads: MP4, MOV, M4V, WebM, MKV, AVI, MPEG-TS and more.

**Are frames the same size as the video?**
Yes, unless you set maxWidth or maxHeight. Rotated phone videos come out upright.

**Which mode for thumbnails?**
Auto: it covers the whole video with its strongest shots and skips black frames. Pick from the contact sheet.

### 🆘 Troubleshooting

- **no video stream in this file** — The link is an audio file.
- **Fewer frames than planned** — Times past the end of the video give no frame and are not billed; RUN_RECEIPT counts them.
- **SOURCE UNAVAILABLE** — None of your links could be reached after retries. Nothing was charged.

### 🔗 Related Actors

- [Bilibili Video Scraper](https://apify.com/nexgensignal/bilibili-video-data-api) — video data by keyword or creator
- [Brand Logo & Favicon Lookup](https://apify.com/nexgensignal/brand-logo-lookup-api) — a company's logo from its domain
- [Website Performance Audit](https://apify.com/nexgensignal/website-performance-audit-api) — Lighthouse scores and web vitals per page

🏢 **About NexGen Signal** — data tools and official-source data products. Browse the catalog: [apify.com/nexgensignal](https://apify.com/nexgensignal)

### ⭐ Found this useful?

If this actor saved you time, a rating on its Apify Store page helps other buyers find it.

Part of NexGen Signal — data tools and official-source data products.

# Actor input Schema

## `videoUrls` (type: `array`):

Direct links to video files (not YouTube or social media pages). Up to 500 per run.

## `mode` (type: `string`):

Auto gives frameCount frames that cover the whole video: the start, the strongest cut in each part of it, then evenly spaced frames if there are not enough cuts. Scene changes and keyframes are found by reading the video; the others are planned from its length.

## `intervalSeconds` (type: `number`):

For 'every N seconds'. 1 = one frame per second; 0.5 = two per second.

## `frameCount` (type: `integer`):

For 'auto' and 'evenly spaced': how many frames per video (evenly spaced with 1 = the middle frame).

## `sceneThreshold` (type: `number`):

For 'scene changes': lower finds more cuts (0.2 sensitive, 0.4 strict).

## `sceneMinGapSeconds` (type: `number`):

For 'scene changes': cuts closer than this are merged (the strongest one is kept), so flashes do not flood the results.

## `timestamps` (type: `array`):

For 'times I list': seconds (12.5), HH:MM:SS (00:01:30.5) or 1m30s.

## `startSeconds` (type: `number`):

Only take frames from this time on.

## `endSeconds` (type: `number`):

Only take frames before this time. 0 = to the end.

## `imageFormat` (type: `string`):

JPEG is smallest for photos, PNG is lossless.

## `quality` (type: `integer`):

1–100. Ignored for PNG.

## `maxWidth` (type: `integer`):

Shrink wider frames to this width, keeping the shape. 0 = full size.

## `maxHeight` (type: `integer`):

Shrink taller frames to this height, keeping the shape. 0 = full size.

## `skipBlankFrames` (type: `boolean`):

When a chosen frame is black, white or one flat colour (a fade, a black intro), step forward up to 2 seconds to the first frame with content.

## `contactSheet` (type: `boolean`):

Also save one grid image per video with up to 100 frames and their timecodes (free, link in every row).

## `saveFrames` (type: `boolean`):

Store each frame in the run's key-value store with a download link. Off = details only.

## `maxFramesPerVideo` (type: `integer`):

Most frames taken from one video.

## `maxVideoMb` (type: `integer`):

Skip video files bigger than this.

## `maxFrames` (type: `integer`):

Stop after this many frames are delivered. Each delivered frame is billed; videos that cannot be read are not.

## Actor input object example

```json
{
  "videoUrls": [
    "https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4"
  ],
  "mode": "auto",
  "intervalSeconds": 10,
  "frameCount": 10,
  "sceneThreshold": 0.3,
  "sceneMinGapSeconds": 1,
  "timestamps": [],
  "startSeconds": 0,
  "endSeconds": 0,
  "imageFormat": "jpg",
  "quality": 90,
  "maxWidth": 0,
  "maxHeight": 0,
  "skipBlankFrames": true,
  "contactSheet": true,
  "saveFrames": true,
  "maxFramesPerVideo": 500,
  "maxVideoMb": 1000,
  "maxFrames": 10
}
```

# Actor output Schema

## `results` (type: `string`):

The delivered (and billed) records.

## `receipt` (type: `string`):

Per-link outcome, robots result, delivered and charged counts.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "videoUrls": [
        "https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4"
    ],
    "mode": "auto",
    "contactSheet": true,
    "maxFrames": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("nexgensignal/video-frame-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "videoUrls": ["https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4"],
    "mode": "auto",
    "contactSheet": True,
    "maxFrames": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("nexgensignal/video-frame-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "videoUrls": [
    "https://images-assets.nasa.gov/video/KSC_69-71212-sRGB/KSC_69-71212-sRGB~small.mp4"
  ],
  "mode": "auto",
  "contactSheet": true,
  "maxFrames": 10
}' |
apify call nexgensignal/video-frame-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nexgensignal/video-frame-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/x4eQ0fDMnO9TMcAqL/builds/lI4YDaoLdmAMcCfuO/openapi.json
