# Video Frames & Thumbnails Extractor (Scenes, Keyframes) (`tinplatelabs/video-frames`) Actor

Extract still frames from your videos: every N seconds, N evenly spaced, exact timestamps, keyframes or scene changes, plus a best-thumbnail pick and contact sheets. JPEG/PNG/WebP with timestamps and ffprobe metadata. URLs or key-value stores. Priced per frame.

- **URL**: https://apify.com/tinplatelabs/video-frames.md
- **Developed by:** [tinplatelabs](https://apify.com/tinplatelabs) (community)
- **Categories:** Videos, AI, Developer tools
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 Frames & Thumbnails Extractor (Scenes, Keyframes)

Pull still frames out of **your video files** the way a pipeline or AI agent can use them: **N evenly spaced
frames**, **one every N seconds**, frames at **exact timestamps**, every **keyframe (I-frame)**, the first frame of
every **scene** (cut detection), or the single **best thumbnail**. Optionally a **contact sheet** (grid image).
Frames are saved as **JPEG, PNG or WebP** with download links; every frame comes with its **timestamp**, timecode,
index and size, and every video with its **metadata** (duration, fps, resolution, codecs) read by ffprobe.

Good for video thumbnails, storyboards, dataset building for vision models, visual QA of renders, content
moderation queues, scene indexes, and feeding video to multimodal LLMs (Claude, GPT, Gemini) as a handful of
images. Built for people and AI agents (through the [Apify MCP server](https://mcp.apify.com)).

> **Your files only.** This Actor reads video files you supply. It does **not** download from YouTube, TikTok,
> Instagram, Facebook, X, Vimeo, Twitch or other platforms: links to their pages get a `PLATFORM_URL_NOT_SUPPORTED`
> error (free). Upload the file you have the rights to into a key-value store, or give a direct link to it on your
> own storage.

### Why this one

- **Six modes in one Actor.** `count`, `interval`, `timestamps`, `keyframes`, `scenes`, `thumbnail`, all limited to
  a `startTime`/`endTime` range if you like.
- **Accurate times.** Every frame reports the timestamp it was actually taken at (to the millisecond), a timecode
  and the frame number. Timestamps accept seconds, timecodes (`1:02:03.5`) or percentages (`50%`).
- **Fast on long videos.** Sparse picks seek straight to each time instead of decoding the whole file; dense picks
  decode once.
- **Scene detection** with adjustable sensitivity and a minimum scene length, with the scene-change score per frame.
- **Best thumbnail.** Scores several candidates for sharpness, exposure and detail and keeps the best one, skipping
  black fades and title cards at the very start and end.
- **Contact sheet** with timecode labels, up to 100 tiles.
- **Predictable.** Pay per frame, only for videos that worked. Every failure is an error item that says what to
  change, and it costs nothing.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `videos` | array of strings | required | Direct `http(s)` URLs of video files, key-value store records as `kvs://KEY` (the store in `keyValueStoreId`, else this run's store) or `kvs://STORE_ID_OR_NAME/KEY`, or signed Apify record URLs (`?signature=`). Up to 100 per run. |
| `keyValueStoreId` | string | | Your key-value store holding the files (pick it in Console, or pass its ID). The Actor runs with limited permissions and can read only this store and its own. |
| `mode` | `count` | `interval` | `timestamps` | `keyframes` | `scenes` | `thumbnail` | `count` | Which frames to take (see below). |
| `frameCount` | integer 1-5000 | `10` | `count` mode: number of evenly spaced frames (each in the middle of an equal slice). |
| `interval` | string | `"10"` | `interval` mode: seconds between frames (`0.5`, `10`, `1:00`). First frame at the start. |
| `timestamps` | array of strings | | `timestamps` mode: `12.5`, `1:02:03.5`, `02:03`, `50%`. Up to 1000. |
| `sceneThreshold` | integer 1-100 | `30` | `scenes` mode sensitivity in percent; lower finds more changes. |
| `minSceneGapSeconds` | integer 0-3600 | `1` | `scenes` mode: ignore cuts closer than this to the previous one. |
| `thumbnailCandidates` | integer 2-60 | `10` | `thumbnail` mode: how many frames to score. |
| `startTime`, `endTime` | string | whole video | Limit the range: seconds, timecode or percentage. |
| `format` | `jpeg` | `png` | `webp` | `jpeg` | Image format of frames and contact sheet. |
| `quality` | integer 1-100 | `85` | JPEG/WebP quality. |
| `maxWidth` | integer 0-7680 | `1280` | Scale wider frames down (aspect kept, rotation applied). `0` = original size. Never scales up. |
| `contactSheet` | boolean | `false` | Also save one grid image of the frames. |
| `contactSheetColumns` | integer 1-12 | `4` | Tiles per row. |
| `maxFramesPerVideo` | integer 1-5000 | `300` | Cap per video in any mode; extra frames are dropped with a warning. |
| `maxDurationMinutes` | integer 1-600 | `240` | Longer videos get a `TOO_LONG` item (free). |
| `videoTimeoutSecs` | integer 60-7200 | `1800` | Slower videos get a `TIMEOUT` item (free). |

Modes:

- `count`: N frames spread evenly over the video (or the range).
- `interval`: a frame every N seconds.
- `timestamps`: exactly the times you list (sorted, duplicates removed; times after the end are skipped with a warning).
- `keyframes`: every I-frame the encoder placed. Fast for H.264/HEVC; often a good "natural" sampling.
- `scenes`: the first frame plus the first frame after each cut or big visual change, with its `sceneScore` (0-1).
- `thumbnail`: one frame, the best of `thumbnailCandidates`; all candidate scores are in the summary.

```json
{
  "videos": [
    "https://upload.wikimedia.org/wikipedia/commons/3/36/Launch_of_the_Roman_Space_Telescope_%28NASA_livestream%29.webm",
    "kvs://product-demo.mp4"
  ],
  "keyValueStoreId": "my-videos",
  "mode": "scenes",
  "contactSheet": true,
  "format": "webp",
  "maxWidth": 960
}
```

To use a file that isn't online, upload it to a key-value store, pick that store as `keyValueStoreId` and pass
`kvs://KEY`.

### Output

One dataset item per frame (`"type": "frame"`), then one summary item per video (`"type": "video"`). The dataset has
two views: **Frames** (with image previews) and **Videos**.

```json
{
  "type": "frame",
  "source": "https://upload.wikimedia.org/.../Launch_of_the_Roman_Space_Telescope_%28NASA_livestream%29.webm",
  "videoIndex": 0,
  "frameIndex": 3,
  "timestamp": 31.732,
  "timecode": "00:00:31.732",
  "frameNumber": 951,
  "width": 1280,
  "height": 720,
  "format": "jpeg",
  "sizeBytes": 98431,
  "key": "001-Launch_of_the_Roman_Space_Telescope-NASA_livestream-0004.jpg",
  "url": "https://api.apify.com/v2/key-value-stores/.../records/001-...-0004.jpg?signature=...",
  "mode": "scenes",
  "sceneScore": 0.6121
}
```

```json
{
  "type": "video",
  "source": "https://upload.wikimedia.org/.../Launch_of_the_Roman_Space_Telescope_%28NASA_livestream%29.webm",
  "status": "ok",
  "mode": "scenes",
  "framesExtracted": 12,
  "truncated": false,
  "range": {"start": 0.0, "end": 90.674},
  "frameWidth": 1280,
  "frameHeight": 720,
  "video": {"format": "matroska", "durationSeconds": 90.674, "fps": 29.97, "width": 1920, "height": 1080,
            "rotation": 0, "videoCodec": "av1", "pixelFormat": "yuv420p", "bitrate": 747660, "audioCodec": "opus",
            "hasAudio": true, "sizeBytes": 8474217},
  "contactSheet": {"key": "001-...-contact-sheet.jpg", "url": "https://...?signature=...", "width": 2396, "height": 1013,
                   "tiles": 12, "columns": 4},
  "chargedEvents": {"frame-extracted": 12, "scene-scan-minute": 2, "contact-sheet": 1},
  "timingMs": {"download": 900, "extract": 21000, "total": 23500},
  "warnings": []
}
```

- Frame links are signed and work without an Apify token. Keys are `NNN-<file name>-NNNN.<ext>` in the run's
  key-value store.
- `keyframe` is set in keyframes mode, `sceneScore` in scenes mode, `quality` (sharpness, brightness, contrast,
  score) in thumbnail mode, where the summary also has `thumbnail` and `candidates`.

A video that can't be processed gives an error item, and the run carries on:

```json
{"type": "video", "source": "https://www.youtube.com/watch?v=...", "status": "error",
 "error": {"code": "PLATFORM_URL_NOT_SUPPORTED", "message": "Links to youtube.com pages are not supported: this Actor extracts frames from video files you supply ..."}}
```

Error codes: `INVALID_SOURCE`, `PLATFORM_URL_NOT_SUPPORTED`, `ACCESS_DENIED`, `DOWNLOAD_FAILED`, `FILE_TOO_LARGE`,
`NOT_MEDIA`, `NO_VIDEO_STREAM`, `UNSUPPORTED_MEDIA`, `NO_FRAMES`, `INVALID_RANGE`, `TOO_LONG`, `TIMEOUT`,
`SPENDING_LIMIT_REACHED`, `INTERNAL_ERROR`.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| `frame-extracted` | **$0.002** | Each frame saved (any mode except thumbnail) |
| `best-thumbnail` | **$0.01** | `thumbnail` mode: per started 10 candidates scored (default 10 candidates = one event), one frame saved |
| `contact-sheet` | **$0.005** | Each contact sheet |
| `scene-scan-minute` | **$0.01** | `scenes` mode only: each started minute scanned, per 1080p30-sized unit (1080p60 counts 2, 4K30 counts 4) |
| `apify-actor-start` | **$0.0005** per GB | Once per run, per GB of memory ($0.001 at the default 2 GB) |

10 frames from a video cost $0.02; a 10-minute 1080p30 video split into 40 scenes with a contact sheet costs
$0.08 + $0.10 + $0.005 = $0.185. Platform usage is included. Failed videos are never charged. The run's maximum
total charge is respected: the frame cap is lowered to what it still allows, and a video that does not fit is
marked `SPENDING_LIMIT_REACHED`.

### Speed and memory

Seeking costs about the same on a 1-minute or a 3-hour video, so `count`, sparse `interval`, `timestamps` and
`thumbnail` are fast on any length. `scenes` (and VP9/AV1 `keyframes`) decode every frame: measured on the default
2 GB run, 1080p30 AV1 at about 5x real time, 1080p60 VP9 at about real time. More memory means more CPU and faster runs at the same price. The whole file is
downloaded into the run first (up to 2 GB).

### Limits

- 2 GB per file, 100 videos per run, up to 600 minutes per video (`maxDurationMinutes`, default 240), up to 5000
  frames per video (`maxFramesPerVideo`, default 300).
- The first video track is used. Frames are RGB (HDR is tone-mapped by ffmpeg's default conversion, not graded).
- Platform page links (YouTube, TikTok, Instagram, Facebook, X, Vimeo, Twitch and similar) are refused.
- Downloads follow up to 5 redirects; only public addresses are fetched.

### Privacy

Files are downloaded into your run and processed inside it; results go only to the run's dataset and key-value
store. Nothing is sent to any third party, there is no telemetry, and only the exact URLs you give are fetched.

### Use from an AI agent (MCP)

Add the Apify MCP server to your agent and enable this Actor. Call it with
`{"videos": ["<file url>"], "mode": "count", "frameCount": 8, "maxWidth": 768}` and pass the frame `url`s to a
vision model, with each `timestamp` as context.

### Open-source licenses

ffmpeg/ffprobe (LGPL/GPL) is the unmodified Debian package, run as a separate program to read videos. PyAV and
OpenCV are not used.

| Component | License | Role |
|---|---|---|
| Pillow 12.3.0 | MIT-CMU (HPND) | Image encoding, contact sheets |
| numpy 2.4.6 | BSD-3-Clause | Frame scoring |
| httpx 0.28.1, httpcore, h11, anyio, idna | BSD-3-Clause / MIT | Downloads |
| apify 4.1.0, apify-client, crawlee and their dependencies | Apache-2.0 / MIT / BSD | Apify SDK |
| certifi | MPL-2.0 (used unmodified) | CA certificates |
| ffmpeg (Debian package, separate process) | LGPL-2.1+ / GPL | Decoding |

Test-only: pytest (MIT). CI fails on any copyleft Python dependency.

***

Made by Tinplate Labs. This Actor is maintained by an AI; there is no support desk, so every error message is
written to tell you exactly what to change.

# Actor input Schema

## `videos` (type: `array`):

Videos to extract frames from, one per line: direct <b>http(s) URLs</b> of video files, files in a key-value store as <code>kvs://KEY</code> (the store picked below, or this run's store) or <code>kvs://STORE_ID_OR_NAME/KEY</code>, or signed Apify record URLs (with <code>?signature=</code>). MP4, MOV, WebM, MKV, AVI, MPEG-TS, animated GIF and more. Links to YouTube, TikTok, Instagram, Vimeo and other platform pages are <b>not supported</b>. Up to 100 videos per run, 2 GB each.

## `keyValueStoreId` (type: `string`):

Your key-value store that holds the video files. Pick it (or paste its ID) and list its records as <code>kvs://KEY</code>. The Actor runs with limited permissions: besides public URLs it can read only this store and its own run's store.

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

Which frames to take. <b>count</b>: N evenly spaced frames. <b>interval</b>: one frame every N seconds. <b>timestamps</b>: the exact times you list. <b>keyframes</b>: every I-frame (fast). <b>scenes</b>: the first frame and each scene change (cut). <b>thumbnail</b>: the single best frame (sharpest, well exposed) among several candidates.

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

How many evenly spaced frames to take in <code>count</code> mode (1-5000). Each frame is the middle of an equal slice of the video, so black first and last frames are avoided.

## `interval` (type: `string`):

Seconds between frames in <code>interval</code> mode, e.g. <code>10</code>, <code>0.5</code> or <code>1:00</code>. The first frame is at the start.

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

Times to take frames at in <code>timestamps</code> mode: seconds (<code>12.5</code>), timecodes (<code>1:02:03.5</code>, <code>02:03</code>) or percentages of the duration (<code>50%</code>). Up to 1000. Times after the end are skipped with a warning.

## `sceneThreshold` (type: `integer`):

Scene-change threshold in percent (1-100) for <code>scenes</code> mode. Lower finds more cuts (and gradual changes); higher finds only hard cuts. 30 suits most edited video; try 10-20 for slow fades, 40-50 for noisy footage.

## `minSceneGapSeconds` (type: `integer`):

In <code>scenes</code> mode, ignore a scene change closer than this to the previous one (avoids bursts during flashes and fast motion). 0 keeps all.

## `thumbnailCandidates` (type: `integer`):

How many evenly spaced frames <code>thumbnail</code> mode scores (2-60, skipping the first and last 5%). The sharpest, best-exposed, most detailed one is returned; all candidate scores are listed in the summary. Charged as one best-thumbnail event per started 10 candidates.

## `startTime` (type: `string`):

Only use the video from this time on: seconds, a timecode or a percentage (e.g. <code>30</code>, <code>1:30</code>, <code>10%</code>). Empty: from the start.

## `endTime` (type: `string`):

Only use the video up to this time: seconds, a timecode or a percentage. Empty: to the end.

## `format` (type: `string`):

Format of the saved frames and contact sheet.

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

JPEG and WebP quality (1-100). Ignored for PNG.

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

Frames wider than this are scaled down, keeping the aspect ratio (rotation and pixel aspect are applied). 0 keeps the original size. Frames are never scaled up.

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

Also save one grid image of the extracted frames (up to 100 tiles, each labelled with its timecode), charged as one contact sheet. Not used in thumbnail mode.

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

Tiles per row in the contact sheet (1-12). The sheet is at most 2400 px wide.

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

Upper limit of frames saved (and charged) per video, in any mode (1-5000). Extra frames are dropped with a warning and the summary is marked <code>truncated</code>.

## `maxDurationMinutes` (type: `integer`):

Longer videos get a TOO_LONG error item and are not charged (1-600).

## `videoTimeoutSecs` (type: `integer`):

A video whose frames take longer to extract gets a TIMEOUT error item and is not charged (60-7200). Scene detection decodes every frame, so long high-resolution videos need more time or memory.

## Actor input object example

```json
{
  "videos": [
    "https://upload.wikimedia.org/wikipedia/commons/3/36/Launch_of_the_Roman_Space_Telescope_%28NASA_livestream%29.webm"
  ],
  "mode": "count",
  "frameCount": 10,
  "interval": "10",
  "sceneThreshold": 30,
  "minSceneGapSeconds": 1,
  "thumbnailCandidates": 10,
  "format": "jpeg",
  "quality": 85,
  "maxWidth": 1280,
  "contactSheet": false,
  "contactSheetColumns": 4,
  "maxFramesPerVideo": 300,
  "maxDurationMinutes": 240,
  "videoTimeoutSecs": 1800
}
```

# Actor output Schema

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

No description

## `files` (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 = {
    "videos": [
        "https://upload.wikimedia.org/wikipedia/commons/3/36/Launch_of_the_Roman_Space_Telescope_%28NASA_livestream%29.webm"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tinplatelabs/video-frames").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 = { "videos": ["https://upload.wikimedia.org/wikipedia/commons/3/36/Launch_of_the_Roman_Space_Telescope_%28NASA_livestream%29.webm"] }

# Run the Actor and wait for it to finish
run = client.actor("tinplatelabs/video-frames").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 '{
  "videos": [
    "https://upload.wikimedia.org/wikipedia/commons/3/36/Launch_of_the_Roman_Space_Telescope_%28NASA_livestream%29.webm"
  ]
}' |
apify call tinplatelabs/video-frames --silent --output-dataset

```

## MCP server setup

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

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/05cmA8hTWGMYlRCbf/builds/l5d2XTrzob9um4TBh/openapi.json
