# Video Frame Extractor — Thumbnails & Stills from Any Video (`keyman98/video-frame-extractor`) Actor

Extract frames and thumbnails from any direct video file URL (MP4, WebM, MOV, MKV) with ffmpeg running inside the Actor. Evenly spaced frames, one every N seconds, or exact timestamps; JPG, PNG or WebP, optional resize. $1.50 per 1,000 frames; failed files are free.

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

## Pricing

from $1.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.

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 & Thumbnail Extractor

Pull frames or thumbnails out of any video file you can link to directly — pick a fixed number of equally spaced frames, one frame every X seconds, or exact timestamps you choose. Extraction runs with **ffmpeg** inside this Actor's own container: no third-party video API, no watching the video yourself to grab a screenshot.

### What you get (output fields)

One dataset row per extracted frame:

- `videoUrl` — the video URL you requested.
- `frameIndex` — 0-based index of this frame among the frames requested for that video.
- `timestampSec` — the timestamp (in seconds) this frame was taken at.
- `imageUrl` — public link to the extracted frame image in the key-value store.
- `format` — `jpg`, `png`, or `webp`, whichever you asked for.
- `width` / `height` — pixel size of the saved frame, after resizing if you set one.
- `sizeBytes` — size of the saved frame file.
- `videoDurationSec` / `videoWidth` / `videoHeight` / `videoCodec` — the source video's own metadata, from ffprobe, repeated on every row from that video.
- `error` — null on success, or a message explaining why this frame (or the whole video) failed.

### Who it's for

- Anyone who needs a thumbnail for a video they host themselves (a product demo, a webinar recording, a CCTV clip) without opening a video editor.
- Developers building a pipeline that needs frames for a preview grid, a content moderation check, or a computer-vision step, without standing up ffmpeg themselves.
- Anyone who wants a specific still moment from a video — "the frame at 1:32:07" — without downloading and scrubbing the whole file by hand.

### What this Actor is not

**It only reads a direct link to a video file.** Give it a URL that, when fetched, returns video bytes (`.mp4`, `.webm`, `.mov`, `.mkv`, and anything else ffmpeg can demux) — not a page that merely *embeds* or *links to* one. It does not log into YouTube, Vimeo, TikTok, Instagram, or any other video platform to pull a stream out of a player page, and it never will: those platforms' own terms restrict scraping their video streams, and this Actor deliberately stays out of that. If you point it at a web page instead of a file, you get a clear, free error row telling you so — see "If a video or frame fails" below.

### How to use

1. **List your video URLs** — direct links to `.mp4`, `.webm`, `.mov`, `.mkv`, or any other ffmpeg-readable container.
2. **Pick a mode**:
   - `count` (default) — N frames spread evenly across the video (set *Frame count*, default 5).
   - `interval` — one frame every X seconds, starting at 0 (set *Interval seconds*).
   - `timestamps` — frames at exact seconds you list (set *Timestamps*, e.g. `[0, 12.5, 30]`).
3. **Run the Actor.** Each frame becomes one row with a link to the saved image.
4. Optionally set *Output format* (JPEG/PNG/WebP), *Quality*, a *Resize width* (aspect ratio kept automatically), *Max frames per video* (a hard cap regardless of mode), and *Max video size* (skip anything bigger, as a free error row).

### Input example (JSON)

```json
{
  "videoUrls": ["https://example.com/my-video.mp4"],
  "mode": "interval",
  "intervalSeconds": 10,
  "outputFormat": "jpg",
  "jpegQuality": 85,
  "maxFramesPerVideo": 50
}
```

### Output example (JSON)

```json
{
  "videoUrl": "https://example.com/my-video.mp4",
  "frameIndex": 2,
  "timestampSec": 20.0,
  "imageUrl": "https://api.apify.com/v2/key-value-stores/.../records/....jpg",
  "format": "jpg",
  "width": 1920,
  "height": 1080,
  "sizeBytes": 184213,
  "videoDurationSec": 83.87,
  "videoWidth": 1920,
  "videoHeight": 1080,
  "videoCodec": "vp9",
  "error": null
}
```

### If a video or frame fails

- A video that can't be downloaded, isn't reachable within the time/size limits, or turns out to be an HTML page instead of a video file gets **one error row** (`frameIndex`/`timestampSec`/`imageUrl` null) — the rest of your list still runs.
- A video that downloads fine but ffprobe can't find a video stream in it (corrupted file, audio-only file, unsupported container) also gets one error row, with a message pointing you back at "use a direct video file URL".
- In `timestamps` mode, a value past the end of the video is skipped with its own error row (`videoDurationSec` tells you how long the video actually is).
- If ffmpeg itself fails to extract one specific timestamp (rare — usually a badly corrupted section of an otherwise fine video), only **that frame** gets an error row; the rest of the video's frames are unaffected.

No row with `error` set is ever charged.

### Pricing

Pay only for frames actually extracted and saved — nothing charged for a failed video, a skipped out-of-range timestamp, or a frame ffmpeg couldn't produce. Pricing model: **pay-per-event**.

| Event | When it's charged | Price |
| --- | --- | --- |
| `frame-extracted` | a frame was successfully extracted and saved | 0.0015 USD |

### Limitations

- **Direct video file links only.** No YouTube/Vimeo/TikTok/social-platform pages, no HLS/DASH streaming manifests — a URL that returns a `.m3u8` or `.mpd` playlist instead of a single video file will fail to probe and come back as an error row.
- **Seeking is fast, not frame-exact.** This Actor seeks with `ffmpeg -ss` *before* `-i` (input seeking), which jumps to the nearest keyframe instead of decoding the whole video up to that point — much faster for videos with many requested frames, but the frame you get back can land a little off the exact timestamp you asked for on a video with sparse keyframes (long GOPs). Frame-exact seeking would mean decoding from the start every time, which does not scale to `maxFramesPerVideo` up to 500.
- **`maxFramesPerVideo` and `maxVideoSizeMb` are hard cutoffs**, not smart sampling: a 3-hour video with the default 50-frame cap in `count` mode still only returns 50 frames, spread across the whole 3 hours.
- **No audio, no scene detection, no deduplication.** This Actor pulls frames at the timestamps you ask for — it does not detect scene changes, skip near-identical frames, or read audio at all.
- Very large videos take proportionally longer to download and probe; `maxVideoSizeMb` (default 500 MB) exists precisely to keep run time predictable.

### FAQ

#### Can it extract frames from YouTube, Vimeo, or TikTok?

No. This Actor only fetches a direct URL to a video file's bytes. It cannot log into or scrape any video platform's player — see "What this Actor is not" above.

#### What video formats are supported?

Anything ffmpeg can demux: MP4, WebM, MOV, MKV, AVI, and more. The format is detected by ffprobe from the file's actual content, not the URL's extension.

#### How much does it cost?

$0.0015 per frame actually extracted. A failed video, a skipped timestamp, or a frame ffmpeg couldn't produce costs nothing.

#### What happens if my timestamp is past the end of the video?

In `timestamps` mode, that timestamp is skipped and reported as a free error row telling you the video's actual duration. `count` and `interval` mode never generate an out-of-range timestamp in the first place.

#### Can I resize the frames?

Yes — set *Resize width* and every frame is scaled to that width, keeping the original aspect ratio (height computed automatically).

#### Why did I get an error row for my whole video instead of one per frame?

That happens for problems that affect the whole file, not one moment in it: the URL couldn't be downloaded, returned something that isn't a video (e.g. an HTML page), or ffprobe couldn't find a video stream in it at all.

#### Can I use this through the Apify API or an MCP server?

Yes, like any Apify Actor — through the standard Apify API, or through the Apify MCP server if you use Claude, Cursor, or another MCP-enabled client.

### Default example video

The input's prefilled example video is *"Launching the Future of Flight, The National Advisory Committee for Aeronautics (Prelude)"* by NASA Ames Research Center, from Wikimedia Commons: `commons.wikimedia.org/wiki/File:Launching_the_Future_of_Flight,_The_National_Advisory_Committee_for_Aeronautics_(Prelude)_(20250220-AAV3550-NACA-Series-Prelude-v1).webm` — public domain (NASA work), no attribution required.

### Export

Results can be downloaded from the Apify dataset as JSON, CSV, or Excel, or accessed via the Apify API.

# Actor input Schema

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

Direct, public URLs to video FILES (mp4, webm, mov, mkv, ...). Not a YouTube/Vimeo/TikTok page, or any web page - the URL must point straight at the video file's bytes.

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

How to pick which frames to extract. 'count': N frames spread evenly across the video. 'interval': one frame every X seconds. 'timestamps': frames at the exact seconds you list.

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

How many equidistant frames to extract per video. Only used in 'count' mode.

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

Extract one frame every this many seconds, starting at 0. Only used in 'interval' mode.

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

Exact timestamps (in seconds, e.g. \[0, 12.5, 30]) to extract a frame at. Only used in 'timestamps' mode. A timestamp past the video's duration is skipped (free, not charged).

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

Hard cap on frames extracted from a single video, regardless of mode (keeps cost and run time predictable).

## `outputFormat` (type: `string`):

Image format for the extracted frames.

## `jpegQuality` (type: `integer`):

Compression quality, 1 (smallest file) to 100 (best quality). Ignored for PNG, which is always lossless.

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

Resize each frame to this width, keeping the original aspect ratio. Leave empty to keep the video's native resolution.

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

Skip (as an error row, not charged) any video larger than this.

## Actor input object example

```json
{
  "videoUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/1/12/Launching_the_Future_of_Flight%2C_The_National_Advisory_Committee_for_Aeronautics_%28Prelude%29_%2820250220-AAV3550-NACA-Series-Prelude-v1%29.webm"
  ],
  "mode": "count",
  "frameCount": 5,
  "intervalSeconds": 5,
  "timestamps": [],
  "maxFramesPerVideo": 50,
  "outputFormat": "jpg",
  "jpegQuality": 85,
  "maxVideoSizeMb": 500
}
```

# Actor output Schema

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

All results in the default dataset (JSON, CSV, Excel).

# 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://upload.wikimedia.org/wikipedia/commons/1/12/Launching_the_Future_of_Flight%2C_The_National_Advisory_Committee_for_Aeronautics_%28Prelude%29_%2820250220-AAV3550-NACA-Series-Prelude-v1%29.webm"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("keyman98/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://upload.wikimedia.org/wikipedia/commons/1/12/Launching_the_Future_of_Flight%2C_The_National_Advisory_Committee_for_Aeronautics_%28Prelude%29_%2820250220-AAV3550-NACA-Series-Prelude-v1%29.webm"] }

# Run the Actor and wait for it to finish
run = client.actor("keyman98/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://upload.wikimedia.org/wikipedia/commons/1/12/Launching_the_Future_of_Flight%2C_The_National_Advisory_Committee_for_Aeronautics_%28Prelude%29_%2820250220-AAV3550-NACA-Series-Prelude-v1%29.webm"
  ]
}' |
apify call keyman98/video-frame-extractor --silent --output-dataset

```

## MCP server setup

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