# Video Frame Extractor: Stills Every N Seconds or at Set Times (`dtkfoundry/video-frame-extractor`) Actor

Extract frames from video links (MP4, WebM, MOV, MKV, HLS .m3u8): evenly spaced video frames, one every N seconds, or at exact times, as JPG, PNG or WebP stills with public links, plus length, fps and codec. Pay per frame.

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

## Pricing

from $1.00 / 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.

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: Stills Every N Seconds or at Set Times

Extract still frames from any direct video link (MP4, WebM, MOV, MKV, AVI, HLS `.m3u8` and most other formats
ffmpeg reads): a set number of evenly spaced frames, one every N seconds, or frames at the exact times you
list. Each frame comes back as a JPG, PNG or WebP image with a public link, ready for AI vision models,
thumbnails, contact sheets or datasets, together with the video's length, size, frame rate and codec.

- **Reliable.** Tested on 200 varied extractions (all three modes, JPG/PNG/WebP, scaling, contact sheets)
  over 96 public videos from 9 hosts (test-video sites, NASA, the Internet Archive, Wikimedia Commons, HLS
  streams from Apple, Mux and Unified Streaming): 195 delivered every frame asked for and no video failed to
  open. The 5 short ones were Wikimedia Commons limiting downloads and one dropped download on the Internet
  Archive; 1,012 of 1,042 frames in all.
- **Only the frames you need are downloaded.** Each frame is taken with a seek, so a frame from minute 40 of a
  2 GB file costs a few megabytes, not the whole file.
- **Exact times.** Frames are decoded at the time you ask for, not snapped to the nearest keyframe.
- **HLS done right.** For a master playlist one video variant is read (the smallest at least `maxWidth`
  wide, else the largest up to 1080p), and fragmented-MP4 HLS streams, which ffmpeg cannot seek, are read
  segment by segment.
- **Three ways to choose frames:** evenly spaced (the black first frame is skipped), every N seconds within an
  optional start/end window, or a list of times like `1:35` or `0:01:35.5`.
- **Free contact sheet** of each video's frames on request.
- **Pay per frame delivered.** Videos that cannot be read and frames that fail are never charged.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `videoUrls` | array | `[]` | Direct links to video files, one per line. Up to 500 per run. Empty = a free format example. |
| `mode` | string | `count` | `count` (evenly spaced), `interval` (every N seconds), `timestamps` (exact times). |
| `framesPerVideo` | integer | `10` | Count mode: frames per video, 1-1000. |
| `intervalSeconds` | string | `10` | Interval mode: seconds between frames. Decimals allowed. |
| `timestamps` | array | `[]` | Timestamps mode: `95`, `1:35`, `0:01:35.5`. Times past the end of a video are skipped, not charged. |
| `startTime` / `endTime` | string | whole video | Count and interval modes: only take frames in this window. |
| `maxFramesPerVideo` | integer | `100` | A cap for every mode, 1-1000. |
| `imageFormat` | string | `jpg` | `jpg`, `png` (lossless) or `webp` (smallest). |
| `imageQuality` | integer | `85` | 1-100, for JPG and WebP. |
| `maxWidth` | integer | `0` | Scale frames down to at most this width (aspect ratio kept). `0` = the video's own size. |
| `contactSheet` | boolean | `false` | Also make one JPG grid of each video's frames, free. |
| `proxyConfiguration` | object | off | Not needed for most links. Apify Proxy or your own, for hosts that refuse cloud servers. |

```json
{
    "videoUrls": [
        "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4",
        "https://example.com/media/product-demo.webm"
    ],
    "mode": "interval",
    "intervalSeconds": "2.5",
    "imageFormat": "jpg",
    "maxWidth": 1280,
    "contactSheet": true
}
```

### Output (one record per frame)

```json
{
    "status": "ok",
    "videoUrl": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4",
    "videoIndex": 1,
    "frameNumber": 2,
    "timestampSeconds": 2.5,
    "timecode": "00:00:02.500",
    "imageUrl": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/001-Big_Buck_Bunny_720_10s_1MB-0002.jpg",
    "imageKey": "001-Big_Buck_Bunny_720_10s_1MB-0002.jpg",
    "imageFormat": "jpg",
    "imageWidth": 1280,
    "imageHeight": 720,
    "imageBytes": 98213,
    "contactSheetUrl": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/001-Big_Buck_Bunny_720_10s_1MB-sheet.jpg",
    "durationSeconds": 10.0,
    "videoWidth": 1280,
    "videoHeight": 720,
    "fps": 30.0,
    "videoCodec": "h264",
    "container": "mov",
    "fileSizeBytes": 1053651,
    "bitrateKbps": 842,
    "extractedAt": "2026-10-21T09:00:00+00:00"
}
```

- The images are stored in the run's key-value store; `imageUrl` is a direct link to each one, and the
  "Images" tab of the run lists them all. Like all run storage on Apify, they are kept for the data retention
  period of your plan; copy them elsewhere if you need them longer.
- For HLS links, `hlsVariantUrl` is the variant playlist that was read.
- `videoWidth` and `videoHeight` are the size the video is shown at (phone videos recorded in portrait are
  reported as portrait).
- A video that cannot be read gives one record with `status: "failed"` and an `error` in plain words (file not
  found, the server refused the download, not a video file, a link to a web page ...). A video where no asked-for
  time falls inside it gives `status: "no-results"`. Neither is charged.

### Pricing

Pay per event, no subscription.

| Event | Price | When it is charged |
|---|---|---|
| Frame | **$0.001** | Once per frame image delivered. 10 frames from each of 20 videos = $0.20. |

Set a maximum charge in the run options and the actor stops before it would go over. Apify's standard
run-start event adds $0.00005 per GB of run memory (the default 1 GB run: $0.00005).

### Limits, stated plainly

- **Direct links to video files only.** Links to pages with a player (YouTube, TikTok, Instagram, Facebook,
  Vimeo, X, Dropbox and Google Drive share pages ...) are not video files; they are reported as failed and not
  charged. Downloading from those sites is against their terms.
- The video has to be reachable from Apify's cloud servers without a login. Some hosts refuse downloads from
  cloud servers (filesamples.com and media.w3.org did in our tests, with HTTP 403), and Wikimedia Commons
  limits how fast files can be downloaded; those videos are reported as failed and not charged. The optional
  proxy setting may help with some hosts.
- Live streams have no fixed length: use timestamps mode for them. DRM-protected streams cannot be read;
  fragmented-MP4 HLS with byte ranges or AES encryption goes to ffmpeg's own HLS reader, which may not seek.
- Speed depends on the video, because each frame is decoded from the keyframe before it. In the test at the
  default 1 GB, a video took 14 seconds (median, about 5 frames); frames from videos up to 640 px wide took
  about 2.5 seconds each and from 1080p about 7.5 seconds. 4K and long-keyframe videos are slower. More run
  memory also gives the run more CPU (1 GB = a quarter of a core), so 2-4 GB is faster for 1080p and 4K.
- PNG frames from 4K video are large (10 MB or more); JPG or WebP, or a `maxWidth`, keep them small.
- No scene-change detection yet: frames are chosen by time.

### Privacy

Only the video links you give are fetched. Frames are stored only in your run's storage. The actor runs with
limited permissions and reads nothing else in your account.

# Actor input Schema

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

Direct links to video files, one per line: .mp4, .webm, .mov, .mkv, .avi, .m3u8 (HLS) and most other formats ffmpeg reads. Links to pages with a player (YouTube, TikTok, Vimeo, Instagram ...) are not video files and are skipped. Up to 500 per run. Leave empty for a free format example.

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

Count = a set number of frames spread evenly over the video. Interval = one frame every N seconds. Timestamps = frames at the exact times you list.

## `framesPerVideo` (type: `integer`):

How many evenly spaced frames to take from each video, 1-1000. Each sits at the middle of an equal slice of the video, so the black first frame is skipped.

## `intervalSeconds` (type: `string`):

One frame every this many seconds, starting at the start time (0 by default). Decimals allowed, e.g. 0.5 or 2.5.

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

One time per line, in seconds or as minutes:seconds or hours:minutes:seconds: "95", "1:35", "0:01:35.5". The same times are used for every video; times past the end of a video are skipped and not charged.

## `startTime` (type: `string`):

Count and interval modes: take frames from this point on (seconds or 1:30 style). Empty = the start of the video.

## `endTime` (type: `string`):

Count and interval modes: take frames up to this point (seconds or 1:30 style). Empty = the end of the video.

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

A cap for every mode, 1-1000. Each frame is one charged result.

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

JPG (small, the default), PNG (lossless, large) or WebP (smallest).

## `imageQuality` (type: `integer`):

1-100 for JPG and WebP (PNG is always lossless). 85 is a good default; lower makes smaller files.

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

Scale frames down to at most this width, keeping the aspect ratio; smaller videos are not scaled up. 0 = the video's own size. 1280 suits most AI vision models.

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

Also make one JPG grid of each video's frames (up to 100, 320 px each), free of charge. Its link is in every frame record as contactSheetUrl.

## `proxyConfiguration` (type: `object`):

Not needed for most links. Turn on Apify Proxy (or give your own proxy) if a host refuses downloads from cloud servers. Proxy traffic is billed by Apify to your account as usual.

## Actor input object example

```json
{
  "videoUrls": [
    "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
  ],
  "mode": "count",
  "framesPerVideo": 10,
  "intervalSeconds": "10",
  "timestamps": [],
  "maxFramesPerVideo": 100,
  "imageFormat": "jpg",
  "imageQuality": 85,
  "maxWidth": 0,
  "contactSheet": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every frame with its time, image link, image size and the video's details.

## `resultsCsv` (type: `string`):

The same frames as CSV.

## `images` (type: `string`):

The frame images (and contact sheets) 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 = {
    "videoUrls": [
        "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
    ]
};

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

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

```

## MCP server setup

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