# Video Frame Extractor - Extract Frames & Video Thumbnails (`pennydata/video-frame-extractor`) Actor

Extract frames from video and make video thumbnails: screenshot frames from a direct MP4 URL every N seconds, N evenly spaced, or at scene changes. JPG/PNG output, optional contact sheet, one dataset row per frame. $0.0008 per frame.

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

## Pricing

from $0.56 / 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 & Contact Sheet

Turn video **file URLs** into still images. Pick a mode: a frame at N-second intervals, N evenly spaced frames, or a frame at each scene change. Each frame is saved as JPG or PNG with a public URL, and you get one dataset row per frame (video URL, timestamp, frame URL, width, height). Optionally get a contact sheet (one grid image per video).

You are charged only for frames that were extracted and saved. Failed downloads, undecodable files and videos with no frames found are free, and so is the contact sheet.

**Not supported:** YouTube, Vimeo, TikTok or other page links. Link the video file itself (mp4, webm, mov, mkv ...). Only use videos you have the right to process.

### Input example

```json
{
  "videoUrls": ["https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"],
  "mode": "interval",
  "intervalSeconds": 2,
  "format": "jpg",
  "contactSheet": true,
  "maxCostUsd": 0.5
}
```

| Field | Meaning |
|---|---|
| `videoUrls` | Direct http(s) links to video files (required). |
| `mode` | `interval` (default), `count`, `scene` or `timestamps`. |
| `timestamps` | Exact times to grab: seconds (`1`, `3.5`) or `mm:ss` / `hh:mm:ss`. Filling it switches to exact-timestamp mode. |
| `startTime` / `endTime` | Window for `interval` and `count` modes (seconds or `hh:mm:ss`). |
| `intervalSeconds` | Mode `interval`: seconds between frames (default 10). |
| `frameCount` | Mode `count`: evenly spaced frames per video (default 8). |
| `sceneThreshold` | Mode `scene`: 0.3 default. Lower finds more cuts. |
| `format` | `jpg` (default) or `png`. |
| `maxWidth` | Scale down frames wider than this (default 1280). Never scaled up. |
| `maxFramesPerVideo` | Upper limit in any mode (default 100). |
| `contactSheet` | Also save a grid image per video (free). |
| `downloadZip` / `maxZipMb` | Also save all frames of the run as one `frames.zip` (free); URL in `zipUrl` on every frame row and in the run output. Frames beyond `maxZipMb` (default 100) are left out of the ZIP only. |
| `maxCostUsd` | Optional spending cap: the run stops once that many frames are extracted. |

### Output

One dataset row per frame:

```json
{
  "videoUrl": "https://example.com/clip.mp4",
  "status": "ok",
  "type": "frame",
  "timestampSec": 2,
  "frameUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/v1-t2000.jpg",
  "width": 960,
  "height": 540,
  "durationSec": 5.06
}
```

The contact sheet is a row with `type: "contact-sheet"`. Failed or skipped videos get a row with `status: "error"` or `"skipped"` and the reason; they are never charged. A `SUMMARY` record in the key-value store lists videos, frames and cost.

### Pricing (pay per event)

| Event | Price |
|---|---|
| `frame-extracted`: one frame saved with a public URL | $0.0008 |

100 frames cost $0.08. Apify's standard platform usage rules apply on top of the event price as shown in the Console. A frame is charged only after it is saved.

### Notes

- Interval and count modes seek to each timestamp, so frames are exact to the nearest decodable frame. Scene mode uses ffmpeg's scene score (`select=gt(scene,X)`); tune `sceneThreshold` for your footage. If none is found the video is reported as failed and not charged.
- Private network addresses are blocked. Files up to 1.5 GB. Frame URLs live in the run's key-value store and follow your Apify storage retention.
- Built with ffmpeg. This Actor was written with the help of an AI (Claude, by Anthropic). It is not affiliated with any video host.

### Switching from another video frame extractor

Paste the input you already use and change only the actor ID. The video link is accepted under any of these keys, as a list, a single string, or `{url}` objects:

| Other actor's input | Used here as |
|---|---|
| `videoUrl`, `url` (single link) | `videoUrls` |
| `videoUrls`, `urls`, `startUrls` (list) | `videoUrls` |
| `frameCount`, `intervalSeconds`, `startTime`, `endTime`, `timestamps`, `maxWidth`, `sceneThreshold`, `contactSheet` | same names here |

Input is compatible; output rows use this actor's own field names (`videoUrl`, `frameUrl`, `timestampSec`), so check your downstream mapping once. Direct video file links only.

### Related actors

- [Audio Transcriber](https://apify.com/pennydata/audio-transcriber): transcribes the audio track of the same video.
- [Shot PDF](https://apify.com/pennydata/shot-pdf): captures web pages rather than video files.
- [Website Image Extractor](https://apify.com/pennydata/website-image-extractor): collects the images on a page, for when the source is a site rather than a video.

Every frame row also carries the video's `durationSeconds`, `width`, `height`, `fps` and `videoCodec` (frame pixel size is `frameWidth`/`frameHeight`); the `SUMMARY` record lists them per video under `videoMeta`.

### Use with AI agents (MCP)

Call this actor from an AI agent (Claude, Cursor, or any MCP client) through the Apify MCP server. Add this URL as an MCP server in your client: https://mcp.apify.com?tools=pennydata/video-frame-extractor
It exposes this actor as a tool the agent can run with the same input as above. An Apify API token is required. This actor was built with AI assistance.

# Actor input Schema

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

Direct http(s) links to video files (mp4, webm, mov, mkv ...). Pages such as YouTube or Vimeo are not supported; link the file itself.

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

interval: one frame every N seconds. count: N evenly spaced frames. scene: a frame at each scene change. timestamps: only the times in Timestamps (also used automatically when Timestamps is filled).

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

Mode interval: seconds between frames.

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

Mode count: number of evenly spaced frames per video.

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

Mode scene: 0.3 is a good start. Lower finds more cuts, higher finds only hard cuts.

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

Exact times to grab, in seconds (1, 3.5) or mm:ss / hh:mm:ss. One frame per timestamp, per video. Times past the end of a video are skipped.

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

Interval and count modes: only use the part of the video after this time (seconds or hh:mm:ss). Default: from the start.

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

Interval and count modes: only use the part of the video before this time (seconds or hh:mm:ss). Default: to the end.

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

jpg is smaller; png is lossless.

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

Frames wider than this are scaled down, keeping the aspect ratio. Never scaled up.

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

Upper limit for any mode. Only extracted frames are charged.

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

Also save one grid image per video with all its frames (free). Its URL is in the dataset row with type contact-sheet.

## `downloadZip` (type: `boolean`):

Also save all extracted frames of the run as one frames.zip in the key-value store (free). Its URL is in the zipUrl field of every frame row and in the run output.

## `maxZipMb` (type: `integer`):

Only with Bundle frames as ZIP: frames beyond this total size are left out of the ZIP (they stay in the dataset and store, and are still charged).

## `maxCostUsd` (type: `number`):

Spending guard: the run stops once this many frames worth of cost are extracted. Leave empty for no cap.

## `videoUrl` (type: `string`):

Same as Video file URLs, for one link; accepted so inputs saved for other video frame extractors run unchanged.

## `url` (type: `string`):

Same as Video file URLs, for one link (alias).

## `urls` (type: `array`):

Same as Video file URLs (alias).

## `startUrls` (type: `array`):

Same as Video file URLs (alias). Strings or {url} objects.

## Actor input object example

```json
{
  "videoUrls": [
    "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
  ],
  "mode": "interval",
  "intervalSeconds": 10,
  "frameCount": 8,
  "sceneThreshold": 0.3,
  "timestamps": [],
  "format": "jpg",
  "maxWidth": 1280,
  "maxFramesPerVideo": 100,
  "contactSheet": false,
  "downloadZip": false,
  "maxZipMb": 100
}
```

# Actor output Schema

## `frames` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `framesZip` (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://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pennydata/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://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"] }

# Run the Actor and wait for it to finish
run = client.actor("pennydata/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://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
  ]
}' |
apify call pennydata/video-frame-extractor --silent --output-dataset

```

## MCP server setup

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