# Video Frame Extractor - Scene Detection, $1.50/1k Frames (`martin10291/video-frame-extractor`) Actor

Video Frame Extractor: still frames from direct video file URLs (mp4, mov, webm, mkv, m4v). Scene-change detection, every N seconds, N evenly spaced frames or exact timestamps. One row per frame with image URL, timecode and video metadata, plus an optional contact sheet. Seeks over HTTP without down

- **URL**: https://apify.com/martin10291/video-frame-extractor.md
- **Developed by:** [Martin Petrov](https://apify.com/martin10291) (community)
- **Categories:** Videos, AI, Integrations
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 results

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

## Extract Frames from Video - Keyframes, Scene Detection, MCP

**Need still images out of video files, at scene changes, every N seconds or at exact timestamps?** Give this actor direct video file links and get one row per frame with the image URL, timecode and video metadata.
**Feeding video to an AI vision model?** Extract a handful of frames plus one contact sheet instead of sending the whole file.

No YouTube downloader, no proxies, no login. Your video is read with ffmpeg and the honest User-Agent `RadarFrames/1.0 (+apify.com/martin10291)`. For interval, count and timestamps modes the actor seeks over HTTP and does **not** download the whole file.

### API example (copy and paste)

```bash
curl -X POST "https://api.apify.com/v2/acts/martin10291~video-frame-extractor/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "videoUrls": ["https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4"],
    "mode": "count",
    "frameCount": 8,
    "format": "jpg",
    "width": 1280,
    "contactSheet": true
  }'
```

Scene changes instead of fixed times: `"mode": "scenes", "sceneThreshold": 0.3`. One frame every 5 seconds: `"mode": "interval", "everySeconds": 5`. Exact times: `"mode": "timestamps", "timestamps": ["0", "12.5", "00:01:30"]`.

### MCP example (Claude, Cursor, any MCP client)

Add the Apify MCP server with this actor enabled:

```
https://mcp.apify.com/?actors=martin10291/video-frame-extractor
```

Then ask your assistant: "Extract 8 evenly spaced frames from https://example.com/demo.mp4 and describe what happens." The tool returns image URLs and the contact sheet URL that a vision model can open.

### Modes

| Mode | What you get |
|---|---|
| `count` | N evenly spaced frames (default 10) |
| `interval` | One frame every N seconds |
| `timestamps` | Frames at exactly the times you list (seconds or HH:MM:SS) |
| `scenes` | A frame at every scene change (ffmpeg scene detection, threshold 0.01 to 1) |

### Input

| Field | Default | Meaning |
|---|---|---|
| `videoUrls` | example clip | Direct links to video files (mp4, mov, webm, mkv, m4v) or Apify key-value store file URLs |
| `mode` | `count` | `count`, `interval`, `timestamps` or `scenes` |
| `frameCount` | 10 | Frames in count mode |
| `everySeconds` | 10 | Step in interval mode |
| `timestamps` | none | Times in timestamps mode |
| `sceneThreshold` | 0.3 | Scenes mode sensitivity, lower = more frames |
| `maxFramesPerVideo` | 30 | Hard cap per video (max 500). Caps your cost |
| `format` | `jpg` | `jpg`, `png` or `webp` |
| `width` | 1280 | Output width, aspect ratio kept, never upscaled. 0 = original |
| `quality` | 85 | jpg / webp quality 1-100 |
| `contactSheet` | true | One tiled grid image per video with timecodes (one extra result row) |
| `maxVideoMinutes` | 30 | Skip longer videos |
| `onlyNew` + `monitorId` | off | Skip videos already processed with the same Monitor ID and settings. Each video is paid for once |

### Output

One dataset row per frame, plus one row of `type: "contact_sheet"` per video when enabled.

```json
{
  "type": "frame",
  "videoUrl": "https://example.com/demo.mp4",
  "frameIndex": 0,
  "timestampSec": 0.5,
  "timecode": "00:00:00.500",
  "imageUrl": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/1-001.jpg",
  "key": "1-001.jpg",
  "width": 640,
  "height": 360,
  "sceneScore": null,
  "mode": "count",
  "durationSec": 10.0,
  "fps": 30.0,
  "videoWidth": 640,
  "videoHeight": 360,
  "extractedAt": "2026-11-04T14:34:25Z"
}
```

`sceneScore` (0 to 1, how strong the cut was) is filled in scenes mode. Images are stored in the run's key-value store and `imageUrl` is the public link to each one.

### Pricing

**$1.50 per 1,000 frames** (one result row = one frame or one contact sheet). No start fee, no per-video fee. For comparison, kantolabs charges $2 per 1,000 frames and seemuapps $0.002 per frame plus $0.01 per video (store pages, 2026-11-04). Failed or skipped videos produce no rows and are not charged.

### Speed

Measured by us (2026-11-04, 5 minute 854x480 webm, 4 parallel ffmpeg seeks, local range-capable server):

| Job | Time |
|---|---|
| 8 frames, count mode | about 10 s |
| 30 frames, count mode | about 14 s |
| 15 png frames, interval mode | about 6.5 s |
| 28 scene-change frames, scenes mode | about 29 s |
| Default example (10 s clip, contact sheet) | 6-9 s |

On a remote host the speed depends on the server: a slow origin (archive.org, 14 minute file) took 27 s for 8 frames and 79 s for 30 frames in count mode. Some hosts (Blender, Wikimedia) rate-limit bursts of connections with HTTP 429; the actor slows down and retries.

### Limits (honest)

- **Direct video file URLs only.** YouTube, TikTok, Instagram, Facebook, X, Vimeo and similar page links are skipped with the message "only direct video file URLs" and listed in `RUN_SUMMARY`. There is no downloader and no proxy.
- The file must be public and the server must allow range requests for fast seeking. If a host rate-limits (HTTP 429) the actor slows down and retries. Hosts that block cloud IPs cannot be read.
- Seeking speed depends on the host and the file. Files with the index at the end or very long keyframe gaps are slower.
- Scenes mode streams the file once, so it downloads and decodes the video up to the frame cap. Use it for clips, use count or interval mode for long videos.
- Max 500 frames per video, default max video length 30 minutes (adjustable up to 600).
- Frames are decoded video pictures, no audio, no OCR, no object detection.

### Errors and monitoring

`RUN_SUMMARY` (key-value store) lists rows, videos ok and failed, notes and `lastError`. A broken link never stops the batch.

### Use it as an integration (chain it after another actor)

This actor can run automatically after any actor whose results contain a direct video link.

1. Open the source actor in Apify Console and go to the **Integrations** tab.
2. Click **Add integration** -> **Actor**, pick this actor and set **Dataset** to `{{resource.defaultDatasetId}}` (leave `videoUrls` empty). Set your other options and save.
3. When the source run finishes, this actor reads its dataset and processes every row. The field is detected automatically; set **Dataset field** if your column has another name.

You can also start it by hand with `datasetId` (pick a dataset or paste its ID) to process an existing dataset.

Good sources to chain from:

- any actor that outputs a direct videoUrl / downloadUrl (mp4, webm)

# Actor input Schema

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

Direct links to video files (mp4, mov, webm, mkv, m4v) or Apify key-value store file URLs. Links must be public and point straight to the file. YouTube, TikTok, Instagram and Facebook page links are not supported and are skipped.

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

scenes = a frame at every scene change (ffmpeg scene detection). interval = one frame every N seconds. count = N evenly spaced frames. timestamps = exact times you list.

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

Used in count mode: how many evenly spaced frames to extract per video (limited by Max frames per video).

## `everySeconds` (type: `number`):

Used in interval mode: extract one frame every this many seconds, starting at 0.

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

Used in timestamps mode: times to extract, as seconds (12.5) or HH:MM:SS (00:01:02.5). Times beyond the video end are skipped.

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

Used in scenes mode: sensitivity from 0.01 to 1. Lower finds more scene changes (0.1 very sensitive), higher only hard cuts. 0.3 is a good default.

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

Hard cap on frames per video (any mode). You pay per frame, so this caps the cost. Maximum 500.

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

Image format of the extracted frames. jpg is smallest, png is lossless, webp is a good compromise.

## `width` (type: `integer`):

Resize frames to this width, keeping the aspect ratio. Never upscales. 0 keeps the original size.

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

Quality for jpg and webp, 1 to 100 (higher = better and bigger files). Ignored for png.

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

Also create one tiled grid image per video with all frames and their timecodes (one extra result row of type contact_sheet). Handy for a quick overview or for sending one image to an AI vision model.

## `maxVideoMinutes` (type: `integer`):

Skip videos longer than this many minutes. A safety guard against huge files and surprise cost.

## `onlyNew` (type: `boolean`):

Skip every video URL already processed in an earlier run with the same Monitor ID and the same settings. Use it for recurring jobs on a folder or feed of videos. Each video is paid for once.

## `monitorId` (type: `string`):

Separate memory for each watch list (used with only-new mode). Use a different ID per feed or customer.

## `datasetId` (type: `string`):

Optional. Pick a dataset (or paste its ID) to read the input from, for example the result of Google Maps Scraper. When this actor runs as an integration, the dataset of the triggering run is used automatically (payload.resource.defaultDatasetId), so you can leave this empty. If set, the values found in the dataset replace the main input list.

## `datasetField` (type: `string`):

Optional. The item field that holds the value, for example website, url or videoUrl (dot paths like author.url work). Empty = detected automatically.

## `datasetMaxItems` (type: `integer`):

Upper limit of unique values taken from the dataset, to keep the cost under control.

## Actor input object example

```json
{
  "videoUrls": [
    "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4"
  ],
  "mode": "count",
  "frameCount": 8,
  "everySeconds": 10,
  "timestamps": [],
  "sceneThreshold": 0.3,
  "maxFramesPerVideo": 30,
  "format": "jpg",
  "width": 1280,
  "quality": 85,
  "contactSheet": true,
  "maxVideoMinutes": 30,
  "onlyNew": false,
  "monitorId": "default",
  "datasetMaxItems": 1000
}
```

# Actor output Schema

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

One row per extracted frame (plus one contact_sheet row per video): videoUrl, frameIndex, timestampSec, timecode, imageUrl, key, width, height, sceneScore, durationSec, fps, videoWidth, videoHeight.

# 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"
    ],
    "frameCount": 8
};

// Run the Actor and wait for it to finish
const run = await client.actor("martin10291/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"],
    "frameCount": 8,
}

# Run the Actor and wait for it to finish
run = client.actor("martin10291/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"
  ],
  "frameCount": 8
}' |
apify call martin10291/video-frame-extractor --silent --output-dataset

```

## MCP server setup

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