# Video Frame Extractor — MP4 Frame Extractor & Scene Cuts (`steadyfetch/video-frame-extractor`) Actor

Video frame extractor, no login: still frames from TikTok, Facebook and MP4 video links, charged per frame delivered. Every N seconds, a set count, each scene cut or the exact times you list — one row per frame with its time, size and an image link in your run's storage.

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

## Pricing

from $1.50 / 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 — Frames & Scene Cuts from Any Link

Paste a video link and get its still frames back as image files, one row per frame: every N seconds, a set number spread evenly over the video, one at every scene cut, or at the exact times you list. Works on TikTok and Facebook video links and on any direct video file (.mp4, .mov, .webm, .m3u8) — no login, no cookies, nothing to install. Each row carries the frame's time, its size, the scene-cut score and a link to the image in your run's storage. Click Start with nothing set and you get a sample: five frames of a public-domain film, read live, and nothing is charged for it.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/video-frame-extractor`

- **Actor id:** `steadyfetch/video-frame-extractor`
- **Input:** `{ "videoUrls": ["https://www.tiktok.com/@tiktok/video/7700680369246358815"], "mode": "scenes" }` — the one field you have to set is `videoUrls`.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `1.00` — the run takes only the frames that fit.
- **Price:** from **$1.50 per 1,000 frames** on Gold and above, plus **$0.005 per video** and **$0.004 per started minute of video read**; full table below. A video that gives no frame is never charged.
- **How often it changes:** a published video's frames never change, so a video is read once — run it again only on new links.

*Unofficial, not affiliated with TikTok, Meta, Facebook or any site whose videos you read — not endorsed by or sponsored by any of them.*

### What you get

One row per frame (`type: frame`).

| Column | What it holds |
|---|---|
| `videoUrl` · `videoIndex` | the link you gave and its place in your list |
| `frameIndex` · `timestampSec` · `timestampText` | the frame's number and its time in the video (`timestampText` as `mm:ss`) |
| `sceneScore` | in scenes mode, how strong the cut is (0 to 1); empty in the other modes |
| `requestedSec` | in timestamps mode, the time you asked for that this frame answers |
| `imageUrl` · `imageKey` · `imageBytes` | the frame as a jpg (or png) image in your run's storage, and its size |
| `width` · `height` | the frame's size in pixels (never upscaled) |
| `durationSec` · `videoTitle` · `sourceHost` | the video's measured length, its title where the site gives one, and the site |
| `contactSheetUrl` | with the contact sheet on: one grid image of up to 12 of the video's frames, each labelled with its time |
| `charged` · `chargedEvents` · `status` · `statusReason` | on every row, so the invoice reconciles from the dataset |

A video that gives no frame comes back as one row (`type: video`) saying why — never charged.

### Four ways to pick frames

- **interval** (the default): a frame every `intervalSeconds` (1 by default, from 0.2), from the start, until the video ends or `maxFramesPerVideo` (30 by default, up to 300) is reached.
- **count**: `frameCount` frames spread evenly over the whole video — a quick storyboard of any length.
- **scenes**: the opening frame plus one frame at every scene cut whose change is stronger than `sceneThreshold` (0.3 finds the hard cuts of edited video). Over `maxFramesPerVideo` cuts, the strongest are kept.
- **timestamps**: the first frame at or after each time in `timestamps` — seconds (`75`) or clock times (`1:15`). A time past the video's end is left out and said on the row.

Frames come at `width` 360 px (the default), 720 px, or the video's own size (`original`, up to 1080 px on the short side). Videos up to 30 minutes long.

### Your frames are stored with your run

The images are records in your run's own key-value storage, and each row links to its image. Run storage follows your Apify plan's retention: on the Apify free plan your 10 most recent runs are kept for 4 months, and paid plans keep run data for the retention period set on your plan. Download the frames you need before your run's storage expires.

### What it costs

| Event | Apify free plan | Bronze | Silver | Gold and above |
|---|---|---|---|---|
| Frame (each frame delivered) | $0.002 | $0.0018 | $0.0016 | $0.0015 |
| Video (each video that delivers a frame) | $0.008 | $0.007 | $0.006 | $0.005 |
| Video minute (each started minute of video read) | $0.005 | $0.0045 | $0.0042 | $0.004 |

Minutes counted: the whole video in count and scenes mode; in interval and timestamps mode, only up to the last frame you asked for (the read stops there). A 75-second video with 10 frames at Gold: $0.005 + 2 × $0.004 + 10 × $0.0015 = $0.028. No start fee. Never charged: a link this actor does not read, a video that cannot be opened or decoded, a page with no video, a video over 30 minutes, a video your account already got the same frames of, videos cut by your own limits or the run clock, and the sample a bare Start returns. Platform usage is included in these prices.

### Already delivered, never charged twice

A video your account already got frames of, with the same settings, is skipped and not charged; turn on `includeSeen` to read it again. The memory lives in your own Apify account (a key-value store named `video-frame-extractor-memory`). The same video named twice in one run — even as two different links — is read once and charged once.

### Input

| Field | What it does |
|---|---|
| `videoUrls` | video links, one per line: TikTok video or share links, Facebook videos and reels, Loom, Wistia, Twitch and archive.org video pages, or any direct video file address |
| `mode` | `interval` · `count` · `scenes` · `timestamps` |
| `intervalSeconds` · `frameCount` · `sceneThreshold` · `timestamps` | the setting for each mode |
| `maxFramesPerVideo` | the most frames from one video, any mode (30 by default, up to 300) |
| `width` · `format` | `360` · `720` · `original`; `jpg` or `png` |
| `includeContactSheet` | one labelled grid image per video, no extra fee |
| `includeSeen` | read videos your account already has |
| `maxRunSeconds` | stop after this many seconds; videos not reached get their own uncharged row |

YouTube, Vimeo, Instagram and X pages are not read; a direct file link to the same video works.

### As an HTTP endpoint

The same actor answers HTTP requests (Apify Standby): `GET https://steadyfetch--video-frame-extractor.apify.actor?videoUrl=<link>&mode=count&frameCount=5` with your Apify token returns the frames as JSON in the same response, each image inline (base64), charged exactly as a run. One video per request, at most 10 frames of a video up to 3 minutes long, at 360 or 720 px; Apify free plan callers get 3 frames of a video up to 1 minute at 360 px. The endpoint keeps no account memory and makes no contact sheet.

### When something goes wrong

Every video that gives no frame ends on one uncharged row naming why: the site is not one this actor reads, the link is not a video, the video exists only in a format that cannot be decoded, the page needs a signed-in viewer, it is not found at that address, it is over 30 minutes, or the site refused the read for now (please re-run). A time limit ends the reading, never the delivering: frames already taken are always delivered. This actor may fail when a site changes how it serves video — failed items are never charged.

### Questions

Open an issue on the Issues tab — we usually reply within a couple of hours.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Changelog

This Actor's version history is a separate document: https://apify.com/steadyfetch/video-frame-extractor/changelog.md

# Actor input Schema

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

One video per line: a TikTok video link (tiktok.com/@user/video/…, or a vm.tiktok.com share link), a Facebook video or reel link, a Loom, Wistia, Twitch or archive.org video page, or any direct video file address (.mp4, .mov, .webm, .m3u8). YouTube, Vimeo, Instagram and X pages are not read (their direct file links are). Videos up to 30 minutes long. Each video is charged one `video` fee and its started minutes as `video-minute` only when at least one of its frames is delivered.

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

interval: a frame every "Seconds between frames" from the start · count: "Number of frames" spread evenly over the whole video · scenes: the opening frame and one at every scene cut stronger than "Scene cut sensitivity" · timestamps: the first frame at or after each time in "Timestamps". The `video-minute` fee counts the started minutes the decode reads: the whole video in count and scenes mode, up to the last frame in interval and timestamps mode.

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

interval mode: one frame every this many seconds, from 0:00, until the video ends or "Max frames per video" is reached. 0.2 to 600.

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

count mode: how many frames to spread evenly over the whole video (never more than "Max frames per video").

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

scenes mode: how big a change between two frames counts as a cut, 0.05 (every small change) to 0.9 (only hard cuts). 0.3 finds the hard cuts of edited video. Each row carries the cut's `sceneScore`.

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

timestamps mode: the times to take a frame at — seconds (75, "75s") or clock times ("1:15", "0:01:15.5"). A time past the video's end is left out and said on the row.

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

The most frames taken from one video, in any mode (1 to 300). Each delivered frame is charged as one `frame`.

## `width` (type: `string`):

360 or 720 px wide (height follows the video's shape), or original: the video's own size, from a version up to 1080 px on its short side. Never upscaled.

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

jpg (small, the default) or png (lossless, several times larger).

## `includeContactSheet` (type: `boolean`):

Also make one grid image per video (up to 12 of its frames, each labelled with its time), linked on every frame row as `contactSheetUrl`. jpg at 360 or 720 px only; no extra fee.

## `includeSeen` (type: `boolean`):

Off (the default): a video your account already got frames of, with the same settings, is skipped and not charged. On: read it again and charge it again.

## `maxRunSeconds` (type: `integer`):

Optional: stop taking frames after this many seconds; videos not reached get their own uncharged row.

## Actor input object example

```json
{
  "mode": "interval",
  "intervalSeconds": 1,
  "frameCount": 10,
  "sceneThreshold": 0.3,
  "maxFramesPerVideo": 30,
  "width": "360",
  "format": "jpg",
  "includeContactSheet": false,
  "includeSeen": false
}
```

# Actor output Schema

## `rows` (type: `string`):

One row per frame (`type: frame`): the video link, the frame's number and its time (`timestampSec`, `timestampText`), the scene-cut score in scenes mode, the frame's width and height, and `imageUrl` — the frame as an image record in this run's own storage. Each frame is charged as one `frame`; the FIRST frame row of each video also carries that video's `video` fee and its `video-minute` fees (one per started minute the decode read). A video that gives no frame is never charged. Every row carries `charged`, `chargedEvents` and `status`, so the invoice reconciles from the dataset itself.

## `misses` (type: `string`):

One uncharged row (`type: video`) for each video that gave no frame, saying why: `unsupported_site` (YouTube, Vimeo, Instagram or X pages), `not_media` (not a video, or only in a format that cannot be read), `no_media` (a page with no video), `drm_or_paywall` (needs a signed-in viewer), `unreachable` (not found at that address), `failed_resolve` / `failed_download` (the site refused for now — please re-run), `skipped_size_cap` (over 30 minutes, or a file far too heavy for its length), `no_match` (every time asked is past the end), `skipped_seen` (your account already has these frames), `skipped_duplicate` (the same video twice in one run), `skipped_budget` / `skipped_deadline` (your maximum charge or the run clock) and `input_error`.

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

The frame images themselves, one record per frame (`frame-<video>-<number>.jpg`), plus one `sheet-<video>.jpg` per video when the contact sheet is on. They live in this run's storage and follow your plan's retention.

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

The run's receipt: videos and frames delivered, what was asked for, the charged events and what stopped the run. Uncharged.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/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 = {}

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

```

## MCP server setup

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