# Video Frames and Metadata — scene or interval stills (`hermeskd/video-frames`) Actor

Video file URLs in, each video's metadata and still frames out: at fixed intervals, at scene changes or at the timestamps you name, as JPEG, PNG or WebP images. Pay per frame; scene mode also per minute scanned.

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

## Pricing

from $0.80 / 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

Give it video file URLs; it returns each video's metadata (duration, resolution, frame rate, codecs, audio presence) and still frames at fixed intervals, at scene changes or at the timestamps you name, as image files in the run's key-value store. Use it to look at a video: pass the frames to a vision model. A vision-capable agent cannot watch a video but can look at stills — this Actor turns one video URL into a handful of JPEG, PNG or WebP frames, each with the second it was taken at and a URL to fetch it, plus the facts about the file, in one call. Pricing is per frame and per video read, so a three-frame look costs half a cent; scene mode, which decodes the whole video, is also charged per minute scanned.

### Call it from an agent

Through Apify's MCP server (`https://mcp.apify.com`), tool `call-actor`:

```json
{"actor": "hermeskd/video-frames", "input": {"urls": ["https://media.w3.org/2010/05/sintel/trailer.mp4"], "mode": "interval", "intervalSec": 10, "maxFrames": 3}, "callOptions": {"maxTotalChargeUsd": 0.05, "timeout": 1800}, "waitSecs": 30}
```

The call returns the run's status and its `defaultDatasetId`. Read the records with `get-dataset-items`:

```json
{"datasetId": "<defaultDatasetId from the call>", "limit": 50}
```

Each frame in a record carries `url`, the frame's record in the run's key-value store: fetch it to see the image. As a dedicated tool, add `https://mcp.apify.com?tools=hermeskd/video-frames` to your MCP client. A short video finishes well inside the 30-second wait, so one `call-actor` plus one `get-dataset-items` is the whole exchange. `maxTotalChargeUsd` caps what the run can charge: a video gets only the frames the remaining budget pays for (`charge_limit_capped`), and the URLs after it come back as `charge_limit_reached` records, not charged. In `scenes` mode the budget must also pay for every minute of the video (a video that states its duration and whose minutes it does not cover is `charge_limit_reached`, not scanned and not charged).

### Input

| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `urls` | array of strings | yes | — | Video file URLs (MP4, MOV, MKV, WebM, AVI, MPEG-TS, MPEG, FLV, OGG, MXF), http or https, 1–20 per run, up to 500 MB each. A link to the file itself; playlists (HLS, DASH) and video-platform pages are not read. |
| `mode` | enum `interval` · `scenes` · `timestamps` | no | `interval` | How frames are chosen: `interval` one every `intervalSec` seconds from the start; `scenes` the first frame and every scene change, decoding the whole video (also charged per started minute scanned); `timestamps` the seconds listed in `timestamps`. |
| `intervalSec` | integer | no | 10 | Seconds between frames in `interval` mode, 1–3,600: frames at 0, `intervalSec`, 2 × `intervalSec`, … up to the video's end. |
| `timestamps` | array of numbers | no | — | Seconds from the start for `timestamps` mode, e.g. `[0, 12.5, 30]`; at most 200, each 0 or more. Sorted and deduplicated; those past the video's end give no frame and the record says so. Required in `timestamps` mode. |
| `maxFrames` | integer | no | 20 | At most this many frames per video, 1–200; the earliest are kept. |
| `format` | enum `jpeg` · `png` · `webp` | no | `jpeg` | The frames' image format. |
| `maxWidth` | integer | no | 1280 | Frames wider than this are scaled down to it, keeping the aspect ratio; narrower frames keep their size (never scaled up). 32–7,680; dimensions are always even. |

Every limit above is enforced by the input schema and again in code.

### Output

One record per input URL; the frames themselves are image files in the run's default key-value store.

| Field | Meaning |
|---|---|
| `url` | The input URL. |
| `ok` | `true` when the video was read; `false` with `error`. |
| `metadata` | `{durationSec, width, height, fps, videoCodec, audioCodec, hasAudio, bitrate, sizeBytes}`: duration in seconds; the picture size as displayed (rotation and non-square pixels applied); average frames per second; ffmpeg's codec names (`h264`, `hevc`, `vp9`, `av1`, `aac`, `opus`, …); overall bits per second; the file's size in bytes. A value the file does not state is `null`; `audioCodec` is `null` without audio. |
| `frames` | Each frame as `{timestampSec, key, url, width, height}`, in time order: the second it was taken at (the first frame at or after the time asked for), its record key in the run's default key-value store (`frame-<input index>-<frame index>.<ext>`), its API record URL, and its size in pixels. |
| `minutesScanned` | `scenes` mode only: started minutes of the video the scene pass decoded, each charged as `minute-scanned` — the whole video, or as far as the pass read when it stopped at `maxFrames` or at the charge limit. Absent in `interval` and `timestamps` mode. |
| `warnings` | Present when fewer frames came back than the input asked for: `frames_capped` (more frames were chosen than `maxFrames`; the earliest kept), `timestamps_beyond_end` (some timestamps are past the video's last frame and gave no frame), `charge_limit_capped` (the run's maximum total charge paid for fewer frames, or in `scenes` mode for fewer minutes of a video that states no duration), `frames_unreadable` (some planned frames could not be decoded). |
| `error` | `{code, message}` when `ok` is `false`. |

A video whose metadata was read but that yields no frame — every timestamp asked for is past its end — is `ok: true` with `frames: []` and `timestamps_beyond_end`, and only `video-probed` is charged: the video was read.

Example record, as this Actor returned it on 2026-10-16 for the input of the example task "Extract a still frame every 5 seconds from a video" (`intervalSec: 5`, `maxFrames: 10`), run locally with the image's ffmpeg on the file fetched from that URL; the key-value store's id, set by the platform per run, is shown as `<storeId>`:

```json
{
  "url": "https://raw.githubusercontent.com/intel-iot-devkit/sample-videos/57978890822836f2b4743852f04f62fc511757e4/car-detection.mp4",
  "ok": true,
  "metadata": {"durationSec": 30.16, "width": 768, "height": 432, "fps": 12.5, "videoCodec": "h264", "audioCodec": "aac", "hasAudio": true, "bitrate": 745770, "sizeBytes": 2811553},
  "frames": [
    {"timestampSec": 0.0, "key": "frame-00-000.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frame-00-000.jpg", "width": 768, "height": 432},
    {"timestampSec": 5.04, "key": "frame-00-001.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frame-00-001.jpg", "width": 768, "height": 432},
    {"timestampSec": 10.0, "key": "frame-00-002.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frame-00-002.jpg", "width": 768, "height": 432},
    {"timestampSec": 15.04, "key": "frame-00-003.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frame-00-003.jpg", "width": 768, "height": 432},
    {"timestampSec": 20.0, "key": "frame-00-004.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frame-00-004.jpg", "width": 768, "height": 432},
    {"timestampSec": 25.04, "key": "frame-00-005.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frame-00-005.jpg", "width": 768, "height": 432},
    {"timestampSec": 30.0, "key": "frame-00-006.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frame-00-006.jpg", "width": 768, "height": 432}
  ]
}
```

### Events and prices

Pay per event; platform usage is included in the price, never passed through.

| Event | Title | Charged when | FREE / BRONZE | SILVER | GOLD / PLATINUM / DIAMOND |
|---|---|---|---|---|---|
| `frame-extracted` | Frame extracted | One still frame saved. | $0.001 | $0.0009 | $0.0008 |
| `video-probed` | Video probed | One video's metadata read. | $0.002 | $0.0018 | $0.0016 |
| `minute-scanned` | Minute scanned | One started minute of video decoded by the scene scan (scenes mode only). | $0.015 | $0.0135 | $0.012 |

All are charged after the video's record is written: `video-probed` once per video read, `frame-extracted` once per frame stored, and in `scenes` mode `minute-scanned` once per started minute of video the scene pass decoded (`minutesScanned` on the record; 52 seconds is one minute, 60.1 seconds two). `interval` and `timestamps` mode seek to each frame instead of decoding the video and charge no minutes. Apify also charges its standard `apify-actor-start` event: $0.00005 per GB of run memory (one event, $0.00005, at the default 1,024 MB). The minimum maximum-charge a caller can set is $0.02.

Worked example on the FREE tier, the call above: one video probed ($0.002) and three frames ($0.003), plus $0.00005 for the start — $0.00505 in all. Ten videos with 20 frames each cost 10 × $0.002 + 200 × $0.001 = $0.22, plus the start. In `scenes` mode, a 9 min 56 s film with 40 scene changes is one video probed ($0.002), 10 minutes scanned ($0.15) and 40 frames ($0.04): $0.192, plus the start. Nothing is charged for a failed video.

### Limits and run time

- 1–20 URLs per run, read one after another; files up to 500 MB; at most 200 frames per video and 200 timestamps.
- Video files download at 2 MB/s or faster: each download has a budget of 30 seconds plus 1 second per 2 MB of the file's declared size (of 500 MB when the server declares none), and stops when no byte arrives for 30 seconds; a slower file is `download_failed`. At most 2 requests a second go to one host.
- Frames in `interval` and `timestamps` mode are found by seeking, so their cost does not grow with the video's length: each frame decodes from the keyframe before it. On a container without an index (MPEG-TS, some AVI and MPEG files) a seek that lands off target is redone by decoding forward from 30 seconds before the time asked for, or from the start. A frame is the first one at or after the time asked for.
- `scenes` decodes the whole video once (the frames no other frame depends on are skipped to halve the work); a scene change is a frame whose difference from the one before scores above 0.3 on ffmpeg's scene-change scale, and the first frame is always included. The pass stops at the `maxFrames`th frame. Its run time grows with the video's length and resolution — measured on one core of the build host (Apify's cores may be slower), per minute of H.264 film at 24 fps: about 0.3 seconds of CPU at 320×180, 4 at 1280×720, 7 at 1920×1080 and 25 at 3840×2160; so at the default memory (a quarter of a core) a 10-minute 4K video takes about 17 minutes — give long or 4K videos 4,096 MB (one core) or more. The price per minute scanned is the same at every resolution.
- GitHub release links (`github.com/…/releases/download/…`) are refused with `robots_disallowed`: github.com's robots.txt disallows every crawler not named in it, HermesBot included. Use the file's `raw.githubusercontent.com` address or your own storage.
- Typical run time: measured on the build host, the call above's work — the probe and three frames of a 52-second 854×480 video — is about half a second of CPU on one thread, plus the download; at the default memory (1,024 MB, a quarter of a CPU core) that is a few seconds. The default run timeout is 1,800 seconds.

### Errors

Errors are records with `ok: false` and `error.code`; none is charged. Frames already written to the key-value store for a video that then failed stay there, uncharged.

| `error.code` | Meaning |
|---|---|
| `download_failed` | The URL did not return the file (status or network error), or the download was slower than its budget. |
| `unsupported_media` | Not a decodable video: no video stream (an audio file or a still image), a damaged or truncated file, a playlist or a format outside the supported containers. |
| `too_large` | The file is over 500 MB. |
| `robots_disallowed` | robots.txt forbids fetching the URL. |
| `blocked_host` | The host is on the deny-list (video platforms among them), or resolves to a private or local address. |
| `timeout` | The video did not finish within the run's time budget. |
| `internal_error` | Unexpected failure; the message carries the class. |
| `charge_limit_reached` | The run reached its maximum total charge before this URL; it was not processed. |
| `invalid_input` | The run input passed the input schema but cannot be used (for example `timestamps` mode without `timestamps`); the message names the field. One record per input given, or one with `url: null` when none was given; nothing is processed. |

The run fails only when every input failed with an Actor-side code (`internal_error`, `timeout`), so a broken Actor is visible. Every other run succeeds — including one where nothing was read because of the caller's own inputs (bad URL, robots, deny-list, not a video, invalid input, the charge limit), or where those and Actor-side failures are mixed.

### Sources and terms

The source is the caller's video. Process only videos you have the right to process. Fetching follows the Hermes fetch policy: robots.txt is obeyed for the user-agent `HermesBot` (and `*`), and a disallowed URL is refused without a request; platforms whose terms forbid automated access — video platforms such as YouTube and TikTok among them — and people-search sites are on a deny-list and refused without a request; private and local addresses are refused, after DNS and on every redirect; the user-agent is `HermesBot/1.0 (+https://apify.com/hermeskd)`; no login, cookies, credentials, CAPTCHA solving, evasion or proxies are used. The file is read only as a single video file: playlists and manifests (HLS, DASH, concat lists), which would make the reader fetch further files, are refused. Frames are returned to the caller as they are in the video; no person or face is identified.

### Not affiliated

This Actor is not affiliated with YouTube, TikTok, the FFmpeg project, the Blender Foundation (whose Sintel trailer is the example input) or Intel (whose sample videos are the example tasks' inputs). FFmpeg and Pillow are used under their open-source licences; the example videos are under Creative Commons licences (Sintel CC BY 3.0, Intel's sample videos CC BY 4.0).

### Related Hermes Actors

- `pdf-to-markdown` — PDF documents to Markdown, with OCR for scanned pages.
- `document-to-markdown` — Word, PowerPoint, Excel and other office files to Markdown.
- `image-to-text` — the text in an image, by OCR.
- `audio-to-transcript` — speech to text.
- `image-upscale` — image upscaling.
- `url-to-markdown` — a web page to Markdown.
- `audio-video-convert` — audio and video format conversion.

### Changelog

- 1.0 (2026-10-16) — first release: video metadata and still frames by interval, scene change or timestamp, as JPEG, PNG or WebP in the run's key-value store; scene mode charged per minute scanned as well as per frame.

# Actor input Schema

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

Video file URLs (MP4, MOV, MKV, WebM, AVI, MPEG-TS, MPEG, FLV, OGG, MXF), http or https, 1–20 per run, up to 500 MB each. A link to the file itself, not to a video platform's page; playlists (HLS, DASH) are not read. Each URL becomes one record.

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

How frames are chosen: interval takes one every intervalSec seconds from the start; scenes takes the first frame and every scene change, decoding the whole video (also charged per started minute scanned); timestamps takes the seconds you list in timestamps.

## `intervalSec` (type: `integer`):

Seconds between frames in interval mode, 1–3,600: frames at 0, intervalSec, 2 × intervalSec, … up to the video's end.

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

Seconds from the video's start for timestamps mode, e.g. \[0, 12.5, 30]; at most 200, each 0 or more. Sorted and deduplicated; those past the video's end give no frame and the record says so. Required in timestamps mode, ignored otherwise.

## `maxFrames` (type: `integer`):

At most this many frames per video, 1–200; the earliest are kept.

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

The frames' image format: jpeg (smallest for photos), png (lossless) or webp.

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

Frames wider than this are scaled down to it, keeping the aspect ratio; narrower frames keep their size (never scaled up). 32–7,680; dimensions are always even.

## Actor input object example

```json
{
  "urls": [
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ],
  "mode": "interval",
  "intervalSec": 10,
  "maxFrames": 3,
  "format": "jpeg",
  "maxWidth": 1280
}
```

# Actor output Schema

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

One record per input URL with ok, metadata, frames (timestamp, key, URL, width, height), warnings and error.

## `files` (type: `string`):

The extracted frames as JPEG, PNG or WebP files in the run's key-value store.

# 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 = {
    "urls": [
        "https://media.w3.org/2010/05/sintel/trailer.mp4"
    ],
    "maxFrames": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("hermeskd/video-frames").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 = {
    "urls": ["https://media.w3.org/2010/05/sintel/trailer.mp4"],
    "maxFrames": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("hermeskd/video-frames").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 '{
  "urls": [
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ],
  "maxFrames": 3
}' |
apify call hermeskd/video-frames --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hermeskd/video-frames"
        }
    }
}
```

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/JwagfgvQ3bi0EGaZv/builds/YYhHPVM8OlaUDEK7b/openapi.json
