# Video Frame Extractor - Keyframes, Scenes, Thumbnails for AI (`kantolabs/video-frame-extractor`) Actor

Extract still frames from any video file: scene-change keyframes, evenly spaced thumbnails or one frame every N seconds, plus a contact sheet and metadata. Image links ready for AI vision. $2 per 1,000 frames.

- **URL**: https://apify.com/kantolabs/video-frame-extractor.md
- **Developed by:** [Kanto Labs](https://apify.com/kantolabs) (community)
- **Categories:** Videos, AI, Developer tools
- **Stats:** 105 total users, 82 monthly users, 99.4% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 frames

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 - Keyframes, Scene Changes & Thumbnails from Any Video

Turn video files into **still images you can use**: the real scene changes, a fixed number of
evenly spaced thumbnails, or one frame every N seconds. Every frame gets a **public image link and a
timestamp**, each video gets a **contact sheet** (a storyboard grid) and its metadata (duration,
resolution, fps, codecs).

Built for **AI pipelines**: vision models such as GPT, Claude and Gemini read images, not video -
this actor gives them the right frames in one call. No ffmpeg install, no servers, no API key.
You pay **only per frame extracted**.

### What does Video Frame Extractor do?

Video Frame Extractor downloads each video file you link to, reads its metadata (duration, resolution, fps, codecs) and saves still frames as JPG, PNG or WEBP with a timestamp: the real scene changes, a fixed number of evenly spaced frames, or one frame every N seconds. Every video also gets a contact-sheet grid. You get one dataset row per video with a public link to every frame.

### Use cases

- **Video understanding with LLMs** - send 8-20 keyframes to a vision model to summarize, tag or moderate a video.
- **Thumbnails** - pick the best still for a video page, a course lesson or a product clip.
- **Storyboards and contact sheets** - review hours of footage at a glance.
- **Ad and creative research** - break your own or licensed ad videos into scenes to compare hooks and cuts.
- **Dataset building** - sample frames from recorded footage to train or evaluate computer-vision models.
- **Quality checks** - spot wrong intros or missing slates in a batch of exports (turn `skipBlankFrames` off to also see black frames).

### Who is it for?

- **AI engineers** sending video to vision models (GPT, Claude, Gemini) that read images, not video.
- **Video and content teams** picking thumbnails and making storyboards.
- **Ad and creative researchers** breaking their own or licensed ads into scenes.
- **ML and data teams** sampling frames from footage for datasets.
- **QA teams** checking a batch of video exports at a glance.

### Frame modes

| Mode | What you get | Good for |
|---|---|---|
| `auto` (default) | First frame + the strongest scene cuts, topped up with evenly spaced frames to reach `frameCount` | A representative storyboard of any video |
| `scenes` | Only real scene changes (ffmpeg scene score above the threshold) | Edited videos, trailers, ads |
| `count` | Exactly N evenly spaced frames | Consistent sampling for AI |
| `interval` | One frame every N seconds | Long recordings, lectures, CCTV-style footage |

### Best thumbnails mode - auto-pick the best video thumbnail

Set `mode` to **`bestThumbnails`** and the actor picks the frames a person would choose as the **video thumbnail**,
instead of returning a storyboard. Per video it scores about 40 candidate frames (the encoder's keyframes - the
sharpest pictures in the file, placed at scene cuts - or evenly spaced frames for files with few keyframes) on:

- **Sharpness** (Laplacian variance; the face counts more than the background), **exposure**, **contrast** and **colourfulness**
- **Faces** (YuNet face detector): size, looking at the camera, **eyes open**, mouth not covered (a hand or object in front of it)
- **Composition** (rule of thirds / centred subject), with a small penalty for logos and end credits

Black, blank, blurry and duplicate frames are dropped, the best `thumbnailCount` frames (default 3, max 10) are
kept with a minimum time gap so they are not three copies of one shot, and each one is checked against the next
0.75 s of video (a blink or motion blur on the chosen frame is swapped for a better neighbour).

Each thumbnail comes with its `rank`, `thumbnailScore` (0-100), a `scoreBreakdown` and optional free **crops**
(`thumbnailCrops`: 16:9 for YouTube, 9:16 for Shorts / Reels / TikTok, 1:1, 4:5) centred on the faces or the
main subject. Thumbnails are full resolution up to `maxWidth` (1280 by default - the YouTube thumbnail size; 0 = original).

```json
{
    "videoUrls": ["https://your-bucket.s3.amazonaws.com/uploads/episode-12.mp4"],
    "mode": "bestThumbnails",
    "thumbnailCount": 3,
    "thumbnailCrops": ["16:9", "9:16", "1:1"]
}
```

Cost: each returned thumbnail is one frame at the normal frame price - 3 thumbnails = **$0.006 per video**, so
1,000 videos = $6. The ~40 candidates that were scored and dropped, the crops and the contact sheet are free.
The scoring is automatic and cannot judge everything a person does (an odd mid-word expression, on-screen text you
would rather avoid), which is why it returns several ranked options rather than one.

### How to use it

1. Click **Try for free**.
2. Paste direct links to your video files into **Video URLs**, one per line.
3. Pick a **Mode**: `auto`, `scenes`, `count` or `interval`.
4. Click **Start**, then open the frame links and the contact sheet from the results, or read them through the API.

### Input

| Field | What it does | Default |
|---|---|---|
| `videoUrls` | Direct links to video files (MP4, MOV, WEBM, MKV, AVI, TS...) | 2 sample videos |
| `mode` | `auto`, `scenes`, `count` or `interval` | `auto` |
| `frameCount` | Frames per video for `auto` / `count` | `12` |
| `intervalSeconds` | Seconds between frames for `interval` | `10` |
| `sceneThreshold` | Scene-cut sensitivity (lower = more cuts) | `0.3` |
| `maxFramesPerVideo` | Cap for `scenes` / `interval` | `50` |
| `skipBlankFrames` | Step past black / fade-to-black / single-colour frames (up to 2 s forward); blank frames are dropped and **not charged** | `true` |
| `imageFormat` | `jpg`, `png` or `webp` | `jpg` |
| `maxWidth` | Downscale wider frames (0 = original resolution) | `1280` |
| `contactSheet` | Also save a grid image of all frames (free) | `true` |
| `maxVideoSizeMb` | Skip larger files | `500` |

```json
{
    "videoUrls": ["https://your-bucket.s3.amazonaws.com/uploads/demo.mp4"],
    "mode": "count",
    "frameCount": 8,
    "maxWidth": 1024
}
```

Only **direct file links** are accepted. Pages of video platforms such as YouTube, TikTok or
Instagram are refused on purpose - their terms do not allow downloading.

### Output

One dataset item **per video**. Real output from a test run on the Sintel trailer (the `frames`
array is trimmed to 2 of its 12 frames; `sceneScore` is set when the frame is a detected cut):

```json
{
    "videoUrl": "https://media.w3.org/2010/05/sintel/trailer.mp4",
    "fileName": "trailer.mp4",
    "fileSizeBytes": 4372373,
    "durationSeconds": 52.21,
    "width": 854,
    "height": 480,
    "fps": 24,
    "videoCodec": "h264",
    "audioCodec": "aac",
    "bitrateKbps": 669,
    "container": "mov",
    "mode": "auto",
    "scenesDetected": 6,
    "frameCount": 12,
    "frames": [
        {
            "index": 4,
            "timestampSeconds": 10.877,
            "timecode": "00:00:10.877",
            "sceneScore": null,
            "width": 854,
            "height": 480,
            "bytes": 5121,
            "key": "b670602f-trailer-004-000010877ms.jpg",
            "url": "https://api.apify.com/v2/key-value-stores/YuY6xATAnCD87Tv00/records/b670602f-trailer-004-000010877ms.jpg?signature=..."
        },
        {
            "index": 5,
            "timestampSeconds": 16.5,
            "timecode": "00:00:16.500",
            "sceneScore": 1,
            "width": 854,
            "height": 480,
            "bytes": 29030,
            "key": "b670602f-trailer-005-000016500ms.jpg",
            "url": "https://api.apify.com/v2/key-value-stores/YuY6xATAnCD87Tv00/records/b670602f-trailer-005-000016500ms.jpg?signature=..."
        }
    ],
    "blankFramesSkipped": 0,
    "contactSheetUrl": "https://api.apify.com/v2/key-value-stores/YuY6xATAnCD87Tv00/records/b670602f-trailer-contact-sheet.jpg?signature=...",
    "processingTimeMs": 4841,
    "error": null
}
```

The frame images and the contact sheet are stored in the run's key-value store; the `url` links
open without an Apify login, so you can pass them straight to an AI model or a spreadsheet.

### How much does it cost to extract frames from a video?

Pay per event: **$0.002 per frame** ($2 per 1,000 frames). The contact sheet, the metadata, skipped blank
frames and videos that fail to download or decode are free.

Worked examples:

- 100 product videos x 8 frames each = 800 frames = **$1.60**
- One 2-hour lecture, one frame every 60 s = 120 frames = **$0.24**
- The default demo run above: 24 frames = **$0.048**

Set **Maximum cost per run** and the actor never extracts more frames than that budget allows.

### Integrations and API

Run it from your own code with the official `apify-client` package (Python shown; JavaScript works the same way):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("kantolabs/video-frame-extractor").call(run_input={
    "videoUrls": [
        "https://media.w3.org/2010/05/sintel/trailer.mp4"
    ],
    "mode": "count",
    "frameCount": 8
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

Or get the results in a single HTTP call:

```bash
curl -X POST "https://api.apify.com/v2/acts/kantolabs~video-frame-extractor/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"videoUrls": ["https://media.w3.org/2010/05/sintel/trailer.mp4"], "mode": "count", "frameCount": 8}'
```

- **AI agents (MCP)**: add the Apify MCP server to Claude, ChatGPT, Cursor or any MCP client and the agent can call this Actor as a tool.
- **No-code**: Make, Zapier, n8n, Google Sheets, webhooks and Apify Schedules work out of the box.

### Limits

- **Direct file links only.** YouTube, TikTok, Instagram, Facebook, X, LinkedIn, Vimeo, Twitch and Douyin pages are refused on purpose.
- 500 MB per video by default, up to 4 GB. Up to 200 frames per video.
- `scenes` mode scans every frame, so long videos take longer; `count` and `interval` decode only the frames they need.
- No audio or transcription - images and metadata only.
- `count` never returns more frames than the video really has (a 1-second clip at 30 fps gives at most 30), so you never pay for duplicates.
- Google Drive / Dropbox / OneDrive *share* links open a web page, not the file: use their direct-download link (Dropbox: `?dl=1`).

### FAQ

**Which formats work?** Anything ffmpeg can decode: MP4/H.264, H.265/HEVC, VP9/WEBM, AV1, MOV,
MKV, AVI, MPEG-TS, M4V and more.

**How big can the video be?** 500 MB by default, up to 4 GB with `maxVideoSizeMb`. Long videos in
`scenes` mode take longer because every frame is scanned for cuts; `count` and `interval` only
decode the frames they need.

**Will I get (and pay for) black frames?** No, by default. Many videos fade in from black and scene
cuts often pass through a black frame. With `skipBlankFrames` on (the default), a black or single-colour
frame is replaced by the first frame with content in the next 2 seconds; if there is none, it is
dropped and not charged (`blankFramesSkipped` counts them). Title cards - text on black - are kept.
Turn the option off if you need frames at the exact planned timestamps.

**Can I get the images as files?** Yes - every frame is a record in the run's key-value store
(download them all from the Storage tab or with the API).

**Something is not working?** Open an issue on the Issues tab - it is answered quickly.

### Related Actors by Kanto Labs

- [Video to LLM Context](https://apify.com/kantolabs/video-to-llm-context) - keyframes + transcript per scene in one AI-ready block for ChatGPT, Claude or Gemini
- [Audio & Video to Text](https://apify.com/kantolabs/audio-video-to-text) - Whisper transcripts with SRT/VTT subtitles, $0.008 per audio minute
- [Auto Captions & Lyric Video Maker](https://apify.com/kantolabs/auto-captions-lyric-video) - TikTok-style burned-in captions and karaoke lyric videos
- [AI Image Upscaler](https://apify.com/kantolabs/ai-image-upscaler) - Real-ESRGAN 2x/4x for photos and anime
- [Image to Text OCR](https://apify.com/kantolabs/image-to-text-ocr) - text from photos, screenshots and scanned PDFs
- [Bulk Image Downloader](https://apify.com/kantolabs/bulk-image-downloader) - every image from web pages to links and a ZIP

# Actor input Schema

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

Direct links to video files (MP4, MOV, WEBM, MKV, AVI, M4V, MPEG-TS...), one per line. Links from your own storage, S3/R2/GCS buckets, CDNs or an Apify key-value store all work. Pages of video platforms (YouTube, TikTok...) are not supported.

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

`auto`: first frame + the strongest scene changes, topped up with evenly spaced frames to reach the frame count (best storyboard). `scenes`: only real scene changes (up to the max per video). `count`: exactly N evenly spaced frames. `interval`: one frame every N seconds. `bestThumbnails`: scores ~40 candidate frames (sharpness, exposure, colour, faces, open eyes, composition) and returns only the best few as thumbnails, with face-centred crops.

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

How many frames `auto` and `count` modes return per video.

## `intervalSeconds` (type: `integer`):

Used by `interval` mode.

## `thumbnailCount` (type: `integer`):

Used by `bestThumbnails` mode: how many of the best frames to return per video, best first. Each returned thumbnail is one charged frame; the candidates that were scored and dropped are free.

## `thumbnailCrops` (type: `array`):

Used by `bestThumbnails` mode: extra free crops of each thumbnail, centred on the faces (or the main subject). 16:9 = YouTube, 9:16 = Shorts/Reels/TikTok, 1:1 = square posts, 4:5 = Instagram portrait. A crop that matches the video's own shape is not repeated. Empty list = no crops.

## `sceneThreshold` (type: `string`):

Minimum ffmpeg scene score (0-1) that counts as a cut. Lower finds more, subtler changes.

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

Upper limit for `scenes` and `interval` modes, so a long video cannot produce thousands of frames.

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

When a chosen frame is black (fade-to-black at a cut, a black intro) or one flat colour, step forward up to 2 seconds to the first frame with content; if there is none, drop it. Dropped frames are not charged. Turn off to keep frames exactly at the planned timestamps.

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

Format of the saved frames.

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

Frames wider than this are scaled down (aspect ratio kept). 0 keeps the original resolution. 1280 is plenty for AI vision models.

## `imageQuality` (type: `integer`):

Compression quality for JPEG and WEBP frames.

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

Also save one grid image with all frames and their timecodes (free).

## `contactSheetColumns` (type: `integer`):

Thumbnails per row on the contact sheet.

## `maxVideoSizeMb` (type: `integer`):

Larger videos are skipped (free).

## `downloadTimeoutSecs` (type: `integer`):

Give up on a video that does not download within this time.

## Actor input object example

```json
{
  "videoUrls": [
    "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4",
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ],
  "mode": "auto",
  "frameCount": 12,
  "intervalSeconds": 10,
  "thumbnailCount": 3,
  "thumbnailCrops": [
    "16:9",
    "9:16",
    "1:1"
  ],
  "sceneThreshold": "0.3",
  "maxFramesPerVideo": 50,
  "skipBlankFrames": true,
  "imageFormat": "jpg",
  "maxWidth": 1280,
  "imageQuality": 85,
  "contactSheet": true,
  "contactSheetColumns": 4,
  "maxVideoSizeMb": 500,
  "downloadTimeoutSecs": 300
}
```

# Actor output Schema

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

No description

# 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://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4",
        "https://media.w3.org/2010/05/sintel/trailer.mp4"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kantolabs/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://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4",
        "https://media.w3.org/2010/05/sintel/trailer.mp4",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("kantolabs/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://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4",
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ]
}' |
apify call kantolabs/video-frame-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kantolabs/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/Bo3ffUwsCAPhzMIXY/builds/jyddrAMLvnnGa90x7/openapi.json
