# Video Frame Extractor - Keyframes & Scenes, 100 Videos/Run (`automa-flow/video-frame-extractor`) Actor

Extract frames, keyframes, scene changes and thumbnails from up to 100 video URLs or a Dataset of videos. One row per frame with an image link, one status row per video, per-frame pricing.

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

## Pricing

from $2.00 / 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 Extractor - Keyframes & Scenes, 100 Videos/Run

Turn up to 100 video URLs, or a Dataset of videos from another Actor, into frames, keyframes, scene changes and thumbnails. Every stored frame is one Dataset row with a public image link, and every video gets one status row, so the next step in n8n, Make or an agent reads one flat column instead of unpacking arrays.

Paste direct `https://` links to video files you are allowed to process (your own storage, signed S3, R2 or GCS links, direct Drive download links), or point the run at the Dataset of a previous Actor. The run downloads each file, reads it with ffprobe and renders only the frames it needs, so a frame from minute 50 costs the same as a frame from second 5.

The prefilled input takes four evenly spaced frames from a 5 second CC0 flower video published by MDN (960x540). You should see one `video` row with status `SUCCESS` and four `frame` rows whose `output_url` opens a JPEG. Open the **Frames** view of the Dataset to see them as a table. Run again when the next batch of videos arrives: a schedule, a Drive or S3 trigger in n8n, or the Dataset of the Actor upstream of this one.

**Price:** $0.002 per stored frame plus $0.005 per processed video; the scenes mode adds $0.005 per started minute it scans. Failed videos and failed frames are free. Update history is in [CHANGELOG.md](CHANGELOG.md).

### Frames from a Dataset of videos

The usual chain: a Downloader, scraper or Bulk FFmpeg run leaves a Dataset whose rows hold video URLs. Point this Actor at it:

```json
{
  "mode": "scenes",
  "datasetId": "aaaaaaaaaaaaaaaaa",
  "sourceField": "storage_url",
  "idField": "id",
  "maxFramesPerVideo": 20
}
```

`sourceField` is the top-level column with the video URL: `storage_url` after Parallel Bulk File Downloader, `output_url` after Bulk FFmpeg Media Processor (`proxy` operation), `url` after most scrapers. The run reads up to `maxItems` rows (default 100) starting at `datasetOffset` (default 0). When the Dataset has more rows, `RUN_SUMMARY` reports `nextDatasetOffset` and `nextAction` names the offset for the next run, so a 300-row Dataset is three runs with offsets 0, 100 and 200. `source_index` is the row's position in the Dataset, so generated ids stay stable across those runs. A row with an empty URL becomes `INVALID_INPUT` and the other rows continue. `idField` may hold text or a number; other characters become hyphens.

In an Actor-to-Actor integration or webhook, set `datasetId` to `{{resource.defaultDatasetId}}`. The platform substitutes the id before the run starts, and under limited permissions this Actor can read only the Dataset named in that top-level field. A raw `payload.resource.defaultDatasetId` is not read and ends FAILED with an `INVALID_INPUT` summary that says so. This mode reads a Dataset in your Apify account, never another customer's storage.

### Modes

| Mode | What you get | Settings |
| --- | --- | --- |
| `count` | `frameCount` evenly spaced frames, each in the middle of an equal slice of the video | `frameCount` 1-200, default 8 |
| `interval` | one frame at 0 s, then every `intervalSeconds`, up to `maxFramesPerVideo` | `intervalSeconds` 1-3600, default 10 |
| `timestamps` | frames at the seconds you list per file | `options.timestamps` on each item, 1-200 values |
| `scenes` | one frame at each detected scene change, the strongest `maxFramesPerVideo` cuts in time order, with `scene_score` | `sceneThreshold` 0.1-0.9, default 0.4 |
| `keyframes` | frames at the video stream's keyframes, evenly subsampled to `maxFramesPerVideo` | none |

Settings for every video: `imageFormat` (`jpeg` default, or `webp`), `maxWidth` (64-4096, default 1280; frames are scaled down, never up, and the height follows the aspect ratio), `contactSheet` (one free JPEG that tiles the video's frames, five columns of 320 px thumbnails) and `maxFramesPerVideo` (1-200, default 50; caps `interval`, `scenes` and `keyframes`).

One mode per run. A file's own `options` override the run's settings for that file, for example `{"frameCount": 3}` or `{"imageFormat": "webp"}`. An unknown option name rejects only that file with `INVALID_INPUT` (`INVALID_OPTION`); a bad run-level value is rejected before the run starts.

`timestamps` mode needs items, because a Dataset row has no per-file timestamps:

```json
{
  "mode": "timestamps",
  "items": [
    {"id": "intro", "source": "https://example.com/intro.mp4", "options": {"timestamps": [1.5, 12, 30]}},
    {"id": "demo", "source": "https://example.com/demo.mp4", "options": {"timestamps": [5]}}
  ]
}
```

A timestamp past the end of the video rejects that file before any frame is rendered. A timestamp at the very end returns the last frame. In `scenes` mode a video with no cut above the threshold, such as a static shot, still gets one frame at 10% of its duration and `note` `NO_SCENE_CUTS`, so a quiet video is a `SUCCESS` with one frame, never an empty success. `keyframes` lists keyframe times without decoding the other frames, so it is the fastest mode for long videos.

### Batch example

```json
{
  "mode": "count",
  "frameCount": 12,
  "maxWidth": 960,
  "contactSheet": true,
  "items": [
    {"id": "clip-a", "source": "https://example.com/a.mp4"},
    {"id": "clip-b", "source": "https://example.com/b.mp4"}
  ]
}
```

`id` is optional. Without it, rows are named `item-0000`, `item-0001` and so on by position, so pass a stable `id` when you compare runs. Send `items` or a dataset, not both. Adding a `datasetId` in the Console while the prefilled flower sample is still in `items` uses the dataset; edited items next to a `datasetId` are rejected.

### Output rows and views

Two row kinds share the default Dataset, told apart by `kind`. Field names are snake_case and `schema_version` is 1; fields are only ever added. The values below are illustrative.

A `video` row, exactly one per input video:

```json
{
  "kind": "video",
  "source": "video-frame-extractor",
  "source_id": "clip-a",
  "source_url": "https://example.com/a.mp4",
  "source_index": 0,
  "id": "clip-a",
  "status": "SUCCESS",
  "mode": "count",
  "input": {"bytes": 1200000, "duration_seconds": 9.98, "container": "mov", "width": 1920, "height": 1080, "fps": 29.97, "video_codec": "h264"},
  "frames_requested": 4,
  "frames_stored": 4,
  "first_frame_url": "https://api.apify.com/v2/key-value-stores/STORE/records/artifacts-clip-a-0000-frame-0000.jpg",
  "contact_sheet_url": null,
  "error_code": null,
  "note": null,
  "billed_frames": 4,
  "billed_scan_minutes": 0,
  "billed_videos": 1,
  "scraped_at": "2026-10-20T12:00:00Z"
}
```

A `frame` row, one per stored frame:

```json
{
  "kind": "frame",
  "source_id": "clip-a#0002",
  "id": "clip-a",
  "video_id": "clip-a",
  "frame_index": 2,
  "timestamp_seconds": 6.237,
  "mode": "count",
  "scene_score": null,
  "output_url": "https://api.apify.com/v2/key-value-stores/STORE/records/artifacts-clip-a-0000-frame-0002.jpg",
  "width": 1280,
  "height": 720
}
```

Dataset views:

- **Frames**: frame rows only, with `video_id`, `frame_index`, `timestamp_seconds`, `output_url` as a link, `width`, `height` and `scene_score`. This is the view to send to a vision model or a sheet.
- **Videos**: one status row per video, with the contact sheet link.
- **Failed**: video rows whose `status` is not `SUCCESS`.

Frames are stored in the run's Key-Value Store as `artifacts-<id>-<index>-frame-<nnnn>.jpg` (or `.webp`) and the contact sheet as `artifacts-<id>-<index>-sheet.jpg`; record keys cannot contain slashes. When your account restricts storage access, the links carry a `signature` query parameter, so `output_url` still opens without an Apify token. Anyone with a link can open that image, so share the links as you would share the frames. `fingerprint` hashes the meaningful fields of a row and skips the observation time, the processing time and the download hash, so comparing two runs is a hash check. `input.sha256` is the hash of the downloaded file, so the same file behind a new signed URL can be recognized.

`RUN_SUMMARY` in the Key-Value Store holds the overall status (`SUCCESS`, `PARTIAL`, `ALL_FAILED`, `NO_RESULTS` or `FAILED`), counts per video status, frames requested and stored, charged event counts, the FFmpeg version line, Dataset paging, `retryIds` and `nextAction`. Read it first. `BILLING_RECEIPT` is the charge ledger of the run.

### Pricing

| Event | When it is charged | Price |
| --- | --- | --- |
| `apify-actor-start` | Actor start, once per GB of memory (minimum one) | $0.00005 |
| `frame-extracted` | one frame stored and validated as a JPEG or WebP image | $0.002 |
| `video-processed` | one video that ended `SUCCESS` or `PARTIAL` (download, probe, contact sheet) | $0.005 |
| `scene-minute-scanned` | one started minute of a video decoded by the scene scan, `scenes` mode only | $0.005 |

Examples: the prefill (one video, four frames) is about $0.013. 20 videos with 12 frames each in `count` mode are about $0.58. 50 five-minute videos with 20 scene frames each are about $3.50.

The frames are stored before anything is charged. A failed video, a rejected URL, a frame that fails validation, a retry, a video stopped by the run timeout and the contact sheet are never charged. Before rendering, the run reserves the video's whole price inside `maxTotalChargeUsd`; when the limit cannot cover a video, that video is `BUDGET_EXCEEDED`, uncharged, its frames are not kept, and videos that already succeeded stay in the Dataset.

These are the event charges. This Actor does not add a separate platform-usage line on top of them.

### Limits

- 1-100 videos per run (`maxItems`); concurrency 1-4 videos, and each video renders at most two frames at a time
- 1-200 frames per video
- 256 MiB per file by default, 1 GiB maximum (`maxInputBytesPerItem`); 2 GiB downloaded per run by default (`maxTotalInputBytes`)
- videos longer than `maxDurationSeconds` (default 1 hour, maximum 2 hours) are `TOO_LARGE`, uncharged
- download ports 80 and 443; direct files only
- memory 1024 MB by default, 2048 MB maximum; run timeout 1 hour by default
- the `scenes` scan decodes the whole video: at 1024 MB it took 25-50 seconds per video minute in our tests, and it may run for 1.5 times the video's length (or `processingTimeoutSeconds`, if longer). For videos over 15 minutes, or many videos per run, use 2048 MB or a longer run timeout, or use `keyframes` mode
- no ZIP, S3 or Drive upload of the frames; use the `output_url` links or Parallel Bulk File Downloader

Near the run timeout, downloads and ffmpeg get only the time that is left, and videos that cannot start in time come back as `TIMEOUT` with `error_code` `RUN_DEADLINE`, uncharged, so the run still finishes with its summary. Send those ids again with a longer timeout.

### API, n8n and Make

Start the Actor with the Apify run API and one of the JSON inputs above. In n8n use the Apify node, or an HTTP Request node that POSTs to `https://api.apify.com/v2/acts/automa-flow~video-frame-extractor/run-sync-get-dataset-items?view=frames` with your Apify token in the `Authorization` header: it waits for the run and returns the frame rows. Then send each `output_url` to the next step, for example a vision model that writes one description per scene into a sheet. In Make, use the Apify app's "Run an Actor" module with "wait for finish", then "Get Dataset Items" with view `frames`.

A daily schedule over an upstream Dataset is the usual second run: create a schedule in the Apify Console with the Dataset input above. When the upstream Actor's run finishes, an integration can start this Actor with `datasetId` set to `{{resource.defaultDatasetId}}`. Do not schedule the prefilled flower sample; it is only the first-run check.

Retry rule: send the ids in `RUN_SUMMARY.retryIds` (videos whose `status` is not `SUCCESS`) in a new run. Successful videos are not in that list, so their frames are not paid for twice. A resent `PARTIAL` video is processed and charged again in full. A restarted run (for example after a platform migration) republishes finished rows from its receipt without charging them again.

Ask an agent: "Extract 8 frames from each of these direct video URLs and give me the image links." The direct MCP endpoint is <https://mcp.apify.com?tools=automa-flow/video-frame-extractor>. An agent should call it for direct video file URLs or a Dataset id, never for YouTube, TikTok or other page URLs, and bound the call with `maxItems`, `maxFramesPerVideo` and `maxTotalChargeUsd`. Execution uses the caller's Apify account. Read `RUN_SUMMARY` first; on `PARTIAL` or `ALL_FAILED`, inspect `error_code` on the video rows before retrying only `retryIds`.

### Failure semantics

`SUCCESS` means every planned frame was rendered, starts with JPEG or WebP bytes, was read back by ffprobe with a width and height, and was stored. `PARTIAL` means at least one frame was stored and `error` names the first failed frame; only stored frames are charged. Any other status has no frame rows, `frames_stored` 0 and an `error`.

| Status | Meaning |
| --- | --- |
| `SUCCESS` | Every planned frame stored |
| `PARTIAL` | Some frames stored, the first failure in `error` |
| `INVALID_INPUT` | Bad or duplicate id, unknown option, timestamp past the end, blocked URL shape |
| `SOURCE_NOT_FOUND` | HTTP 404 or 410, or a missing `kvs://` record |
| `SOURCE_BLOCKED` | HTTP 401/403, or a redirect to a blocked address |
| `SOURCE_FAILED` | Other HTTP errors, DNS failure, network error, an HTML page instead of a video |
| `TOO_LARGE` | Byte cap or duration cap |
| `UNSUPPORTED_MEDIA` | Not a readable video, no video stream (`NO_VIDEO_STREAM`), or no duration for `count`/`interval` (`DURATION_REQUIRED`; use `timestamps`) |
| `PROCESSING_FAILED` | ffmpeg failed or no frame passed validation |
| `TIMEOUT` | Download or ffmpeg exceeded its timeout, or the run timeout came first (`RUN_DEADLINE`) |
| `BUDGET_EXCEEDED` | `maxTotalChargeUsd` cannot cover the video |

An unreachable host is `SOURCE_FAILED`; a file that was fetched but holds no video is `UNSUPPORTED_MEDIA`. Those are different outcomes. The run finishes successfully when per-video failures are the only problem, even when every video fails on its own host (`ALL_FAILED`). It fails the whole run when FFmpeg is missing or built with `--enable-nonfree`, when the Dataset or Key-Value Store cannot be written, when a charge returns an uncertain result, or, after delivering every row, when the run timeout stopped every video (`globalFailure` `RUN_DEADLINE`, nothing charged).

If a run fails or looks wrong, open an Issue and include the run URL. Apify shares that run with us automatically, so there is no need to paste your input.

### Security and privacy

Video URLs are downloaded with a normal HTTP client and only then passed to ffmpeg as local files. ffmpeg never receives the URL, a shell string or a filter graph from the input. The client allows `http` and `https` on ports 80 and 443, rejects embedded passwords, localhost, link-local and metadata names, private and reserved IP ranges, IPv6 loopback and unique-local addresses, and checks DNS before the request and on every redirect. The body is streamed and cut at the byte cap. The client sends no cookies and no `Authorization` header. Query values are redacted in logs, and frames, timestamps and URLs are not logged, only counts.

Frames can show people. This Actor does no recognition and sends nothing to an external service; frames stay in your run's Key-Value Store. You need the rights or permission to process the files you submit. This Actor does not download videos from YouTube, TikTok, Instagram, Facebook, X or Vimeo pages and does not run yt-dlp.

### Licensing

The container installs Debian's unmodified `ffmpeg` package and refuses a build that contains `--enable-nonfree`. Debian's FFmpeg is GPL-2 or later because optional GPL components are enabled. The image keeps Debian's copyright file and the installed package version; the source for that binary is the corresponding Debian source package. Frames use the native JPEG encoder and libwebp (BSD-licensed) when present. Details are in [LICENSING.md](LICENSING.md).

FFmpeg is a trademark of the FFmpeg project. This Actor is not affiliated with, endorsed by or sponsored by FFmpeg; the name describes compatibility.

### Technical details

Python 3.12 on the Apify Python image. Every mode becomes a list of timestamps first; each frame is then one short ffmpeg run that seeks to its instant, so the cost per frame does not grow with the video's length. The scene scan decodes once at 320 px width. Downloads use `httpx` with a pinned DNS backend and one retry on network errors and retryable statuses. ffprobe and ffmpeg run as argument arrays with a timeout and a kill on expiry, and temporary files are removed after each video. There is no browser, no proxy product and no external database.

# Changelog

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

# Actor input Schema

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

count: N evenly spaced frames. interval: one frame every intervalSeconds. timestamps: frames at the seconds you list per file in options.timestamps. scenes: frames at detected scene changes. keyframes: frames at the stream's keyframes, evenly subsampled to maxFramesPerVideo.

## `items` (type: `array`):

1-100 direct video files. Use this or a dataset, not both. id is optional. source is an http(s) file URL or kvs://record in this run's Key-Value Store. options may hold timestamps (seconds, timestamps mode) and override frameCount, intervalSeconds, sceneThreshold, maxFramesPerVideo, imageFormat, maxWidth or contactSheet for that file.

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

count mode: this many evenly spaced frames per video. 1-200. Default 8.

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

interval mode: one frame at 0 s and then every this many seconds, up to maxFramesPerVideo. 1-3600. Default 10.

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

scenes mode: ffmpeg scene change score from 0.1 (sensitive) to 0.9 (only hard cuts). Default 0.4. A video with no cut above it stores one frame at 10% of its duration with note NO_SCENE_CUTS.

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

Cap for interval, scenes and keyframes modes. scenes keeps the strongest cuts. count and timestamps are capped by their own counts. 1-200. Default 50.

## `imageFormat` (type: `string`):

jpeg (default) or webp.

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

Frames wider than this are scaled down, never up; the height follows the aspect ratio. 64-4096. Default 1280.

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

Also store one JPEG that tiles the video's frames, five columns of 320 px thumbnails. Free.

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

Apify dataset to read instead of items. The run requests read access to this field. In an integration, set it to {{resource.defaultDatasetId}}. A nested resource id is not read.

## `sourceField` (type: `string`):

Top-level field on each dataset row that holds the video URL. Required in dataset mode. storage_url after Parallel Bulk File Downloader, output_url after Bulk FFmpeg, url after most scrapers.

## `idField` (type: `string`):

Optional top-level field used as the video id. Text or a number; other characters become hyphens. Rows without it get item-0000 style ids.

## `datasetOffset` (type: `integer`):

Dataset mode only. Skip this many rows, then read up to maxItems. When rows remain, RUN_SUMMARY gives nextDatasetOffset for the next run. Default 0.

## `maxItems` (type: `integer`):

Maximum videos to accept from items or a dataset. 1-100. Default 100.

## `resource` (type: `object`):

Optional webhook object. This Actor does not read defaultDatasetId. Put the id in datasetId.

## `payload` (type: `object`):

Optional webhook wrapper. A nested resource.defaultDatasetId is not read. Put the id in datasetId.

## `concurrency` (type: `integer`):

Videos processed at once (1-4). Each video renders at most two frames at a time. Default 2.

## `maxInputBytesPerItem` (type: `integer`):

Reject a video whose Content-Length or streamed body exceeds this size. Default 256 MiB. Maximum 1 GiB.

## `maxTotalInputBytes` (type: `integer`):

Stop accepting further videos once the downloaded bytes in this run would pass this total. Default 2 GiB. Must be at least the per-video cap.

## `maxDurationSeconds` (type: `integer`):

Videos longer than this are TOO_LARGE and not charged. Default 3600. Maximum 7200.

## `downloadTimeoutSeconds` (type: `integer`):

Total time allowed to download one video, including redirects and the one retry. Default 120.

## `processingTimeoutSeconds` (type: `integer`):

Maximum time one ffprobe or ffmpeg step may run for a video. The process is killed when this expires. The scenes scan decodes the whole video and may also run for 1.5 times its length. Default 600.

## Actor input object example

```json
{
  "mode": "count",
  "items": [
    {
      "id": "flower",
      "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
    }
  ],
  "frameCount": 4,
  "intervalSeconds": 10,
  "sceneThreshold": 0.4,
  "maxFramesPerVideo": 50,
  "imageFormat": "jpeg",
  "maxWidth": 1280,
  "contactSheet": false,
  "datasetOffset": 0,
  "maxItems": 100,
  "concurrency": 2,
  "maxInputBytesPerItem": 268435456,
  "maxTotalInputBytes": 2147483648,
  "maxDurationSeconds": 3600,
  "downloadTimeoutSeconds": 120,
  "processingTimeoutSeconds": 600
}
```

# Actor output Schema

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

No description

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

No description

## `runSummary` (type: `string`):

No description

## `billingReceipt` (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 = {
    "mode": "count",
    "items": [
        {
            "id": "flower",
            "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
        }
    ],
    "frameCount": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/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 = {
    "mode": "count",
    "items": [{
            "id": "flower",
            "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4",
        }],
    "frameCount": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/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 '{
  "mode": "count",
  "items": [
    {
      "id": "flower",
      "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
    }
  ],
  "frameCount": 4
}' |
apify call automa-flow/video-frame-extractor --silent --output-dataset

```

## MCP server setup

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