# Video Frame Extractor - Thumbnails, Scenes & Contact Sheet (`quietworkslab/video-frame-extractor`) Actor

Extract thumbnails and frames from videos: evenly spaced frames, a frame every N seconds, scene changes or keyframes, saved as WebP, JPEG or PNG with exact timestamps, plus a contact sheet per video. Reads MP4, MOV, WebM, MKV and AVI from direct file links, your key-value store or uploaded files.

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

## Pricing

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

## Video Frame Extractor - Thumbnails, Scenes & Contact Sheet

Extract frames from your videos as **WebP, JPEG or PNG** images with their exact timestamps: a number of evenly spaced thumbnails, one frame every N seconds, a frame at every **scene change**, or the video's **keyframes**. Each video also gets a **contact sheet** (one grid image of its frames with their times). The Actor reads MP4, MOV, WebM, MKV and AVI files from **direct file links**, from a key-value store in your Apify account, or from files you upload.

Typical uses:

- Make thumbnails and preview images for a video library, a CMS or an online shop.
- Pick one frame per shot (scene changes) to review, tag or describe a video, for example before sending the frames to a vision model.
- Get a frame every few seconds for a storyboard, a QA check of a render, or a time-lapse overview.
- Turn the screen recordings or product videos another Actor saved into images, right after it finishes.

Only use videos you have the right to use.

### Try it in one click

Click **Start** without adding links, choosing a store or uploading files. The Actor uses a built-in 6-second sample clip made from ffmpeg's synthetic test patterns (a moving test card, color bars, a plain dark scene and a color gradient; no camera footage). There are no `frame-extracted` events for the sample; Apify's Actor start event still applies.

You get 6 evenly spaced WebP frames (at 0.52, 1.52, 2.52, 3.52, 4.52 and 5.52 s; the test card shows its own clock, so you can check the times), one dataset record per frame, a contact sheet `001-sample-clip-contact-sheet.webp`, and a summary in `OUTPUT`. Switch **Which frames** to *Scene changes* to see the four scenes found at 0, 2.0, 3.4 and 4.72 s.

### How it works

1. **Videos in**: direct links (one GET per link, see [Video links](#video-links)), a **key-value store** (optionally only keys with a prefix) and/or **uploaded files**. The type is detected from the file's first bytes, not from its name. Records that are not videos (JSON, text, images) are skipped. Each video is streamed to the run's disk, never held in memory as a whole, and deleted when it is done.
2. **Checks before anything is charged**: the input is validated first (wrong values, a prefix without a store, a store given only in an integration's `payload`, links that are not http/https). For each video, ffprobe reads its track, size, frame rate, rotation and length; the Actor then checks that the frames fit into the run's memory and that your maximum cost per run still covers at least one frame plus the video's expected extra events. A video that fails any check gets a failed record and is not charged; the run goes on with the next one. A run where no video could be used fails with the reasons (nothing charged).
3. **Frames**:
   - *A number of evenly spaced frames* (`count`): the video (or the part between `startSec` and `endSec`) is split into that many equal parts and the frame in the middle of each part is taken, so neither a black first frame nor the very end is used. Each frame is found by seeking: ffmpeg jumps to the nearest keyframe before that time and decodes up to it, so long videos stay fast. The Actor first reads where the keyframes are (without decoding); when they are so few that seeking would decode more than the whole video (typical for screen and browser recordings), it reads the video once from start to end instead and keeps the same frames seeking would keep: the first frame at or after each time, and a frame that several times fall on only once (so a video that stood still, or a low frame rate, gives fewer frames, never extra ones).
   - *One frame every N seconds* (`interval`): at the start and then every N seconds, the same way.
   - *Scene changes* (`scenes`): the whole range is decoded, and the first frame plus every frame that differs from the one before by more than the threshold are kept, up to the maximum per video.
   - *Keyframes* (`keyframes`): only the keyframes are decoded; when there are more than the maximum, they are spread over the video.
4. **Images**: frames are turned upright (rotated phone videos), get square pixels, are shrunk to `maxWidth` (never enlarged) and saved in the format you choose, without metadata. The reported time is the exact time of the decoded frame.
5. **Contact sheet**: one grid image per video with up to 100 tiles and their times.
6. **Results**: the images go to the run's default key-value store, one record per frame to the dataset, and the facts of each video to `OUTPUT`.

### Input

| Field | What it does |
|---|---|
| `videoUrls` | Direct links to video files (MP4, MOV, WebM, MKV, AVI). Not web pages or streaming sites. Each file also counts one event per 64 MB. See [Video links](#video-links). |
| `keyValueStoreId` | Key-value store in your account that holds videos. Each video also counts one event per 16 MB read. |
| `keyPrefix` | Only handle records whose key starts with this text, for example `clip-`. |
| `uploadedFiles` | Files uploaded in the input form. See [Uploaded files](#uploaded-files). |
| `mode` | `count` (default), `interval`, `scenes` or `keyframes`. |
| `frameCount` | `count` mode: frames per video, at most `maxFramesPerVideo`. Empty = 10 (6 for the built-in sample). |
| `intervalSec` | `interval` mode: seconds between frames, default 10. |
| `sceneThreshold` | `scenes` mode: 0.01-1, default 0.3. Lower finds more (also soft) changes, higher only hard cuts. |
| `maxFramesPerVideo` | Upper limit per video in every mode, default 50, at most 500. |
| `startSec`, `endSec` | Optional part of the video to use. |
| `format` | `webp` (default), `jpeg` or `png`. |
| `maxWidth` | Default 1280, 32-3840. Frames are never enlarged. |
| `quality` | 1-100, default 80 (WebP and JPEG). |
| `contactSheet` | Default `true`: one grid image per video. Not charged. |

Example input:

```json
{
    "videoUrls": ["https://example.com/videos/product-demo.mp4"],
    "mode": "scenes",
    "sceneThreshold": 0.3,
    "maxFramesPerVideo": 40,
    "format": "webp",
    "maxWidth": 1280,
    "contactSheet": true
}
```

#### Video links

- The link must return the **file itself** (usually ending in `.mp4`, `.mov`, `.webm`, `.mkv` or `.avi`). A link to a page that shows a video, a streaming site or a playlist (`.m3u8`, `.mpd`) is refused with `NOT_A_VIDEO`.
- Each link is downloaded with **one GET request**; links inside the file are never followed. Up to 5 redirects are followed, and each target is checked again. A failed download is retried only after a server error (5xx) or a broken connection, at most twice. `429 Too Many Requests` is not retried.
- The only header sent is `User-Agent: video-frame-extractor (Apify Actor)`. The Actor does not log in, send cookies or imitate a browser: a link that needs a login fails with `AUTH_REQUIRED`. Use a public link, or upload the file or put it in a key-value store instead.
- Links to private, local or reserved network addresses (for example `10.x.x.x`, `192.168.x.x`, `127.0.0.1`, `169.254.169.254`), also after a redirect, are refused with `BLOCKED_ADDRESS`.
- A link that carries a token in its query string (a signed link) works; the dataset and the log show links without their query string.
- A server that sends neither a `Content-Length` nor chunks ends the file by closing the connection, so a cut file can look complete. A video from such a server that stops before the end its own container declares fails with `DOWNLOAD_FAILED` and is not charged.
- A refused answer (an error, a web page, a file over the size limit) is closed at once, without downloading the rest. One download may take at most 10 minutes and must average at least 1 MB/s after its first minute (`SLOW_DOWNLOAD` otherwise, not charged): put files from slow hosts in a key-value store or upload them instead. The same link given twice is downloaded once.

#### Uploaded files

Apify stores each upload in a new key-value store of your account. This Actor runs with limited permissions: it may always open a store you choose in **Key-value store**, but not necessarily the store of an upload. Accounts keep storages private by default, and then the first run cannot open the uploads. It names the store they are in, for example:

> The run may not open 1 uploaded file: it is in your key-value store AbCdEf1234567890x, and Apify runs this Actor with limited permissions, so it may open only the store you choose. Choose AbCdEf1234567890x in "Key-value store" (pick it, or paste the ID) and start again; the videos uploaded to it are then handled once. No frame-extracted events were charged for it.

Choose that store in **Key-value store** and start again. Uploads that are in the chosen store are not handled a second time.

### Output

Each dataset record is one frame:

```json
{
    "status": "extracted",
    "source": "url",
    "video": "https://example.com/videos/product-demo.mp4",
    "videoNumber": 1,
    "frameIndex": 3,
    "timestampSec": 12.48,
    "timecode": "00:00:12.480",
    "width": 1280,
    "height": 720,
    "format": "webp",
    "bytes": 48213,
    "imageKey": "001-product-demo-0003.webp",
    "imageUrl": "https://api.apify.com/v2/key-value-stores/RUN_STORE_ID/records/001-product-demo-0003.webp",
    "sceneScore": 0.512,
    "chargedEvents": 1,
    "errorCode": null,
    "errorMessage": null
}
```

- `timestampSec` is the time of the decoded frame (3 decimals); with a 25 fps video, asking for 0.5 s gives the frame at 0.52 s. `sceneScore` is set in `scenes` mode (0 for the first frame), `null` otherwise.
- Image keys are `<video number>-<file name>-<frame number>`, so they sort by video and time and never collide. The contact sheet is `<video number>-<file name>-contact-sheet.<format>`.
- A video that failed has one record with `"status": "failed"`, `frameIndex: null`, an `errorCode` (`NOT_A_VIDEO`, `AUTH_REQUIRED`, `NOT_FOUND`, `RATE_LIMITED`, `HTTP_ERROR`, `DOWNLOAD_FAILED`, `TIMEOUT`, `FILE_TOO_LARGE`, `BLOCKED_ADDRESS`, `BLOCKED_URL`, `DNS_FAILED`, `TOO_MANY_REDIRECTS`, `SLOW_DOWNLOAD`, `READ_FAILED`, `CORRUPT_VIDEO`, `NO_VIDEO_STREAM`, `RANGE_OUTSIDE_VIDEO`, `TOO_LITTLE_MEMORY`, `NOT_ENOUGH_TIME`, `NO_FRAMES`, `EXTRACTION_FAILED`, `SAVE_FAILED`, `DISK_FULL`) and a message that says what to do. A video that fails before any frame is saved is not charged; frames saved before a failure keep their records and are charged.
- When a video gives fewer frames than asked for, or has something to know (an HDR video), its entry in `OUTPUT.videos` has a `note`, and the run's message says how many videos have one.

The `OUTPUT` record holds the summary and, for each video, its facts:

```json
{
    "status": "COMPLETE",
    "source": "url",
    "message": "Extracted 12 frames from 1 of 1 video (0 failed); 1 contact sheet.",
    "settings": { "mode": "count", "frameCount": 12, "maxFramesPerVideo": 50, "format": "webp", "maxWidth": 1280 },
    "counts": { "videos": 1, "videosDone": 1, "videosFailed": 0, "frames": 12, "contactSheets": 1, "skippedNotVideos": 0 },
    "events": { "frames": 12, "decode": 0, "data": 1.407 },
    "chargedEvents": 14,
    "videos": [
        {
            "number": 1, "source": "url", "video": "https://example.com/videos/product-demo.mp4", "status": "done",
            "container": "mp4", "durationSec": 60, "fps": 30, "codec": "h264", "width": 1920, "height": 1080,
            "outputWidth": 1280, "outputHeight": 720, "fileBytes": 90048000, "frames": 12,
            "contactSheetKey": "001-product-demo-contact-sheet.webp", "decodedFrames": 398, "decodePlan": "seek",
            "extraEvents": { "decode": 0, "data": 1.407 }, "note": null, "errorCode": null, "errorMessage": null
        }
    ]
}
```

### Run it after another Actor

If another Actor saves videos to its default key-value store (for example screen recordings), extract frames automatically when it finishes:

1. Open that Actor (or its saved task) in Apify Console and go to its **Integrations** tab.
2. Add this Actor as an integration that runs when the run succeeds.
3. In the integration's input, set:

```json
{
    "keyValueStoreId": "{{resource.defaultKeyValueStoreId}}",
    "mode": "count",
    "frameCount": 5
}
```

`{{resource.defaultKeyValueStoreId}}` is replaced with the key-value store of the run that just finished; its JSON records (such as `INPUT` and `OUTPUT`) are skipped. Always set `keyValueStoreId` like this: the Actor may only read a store chosen in that field. If it is empty and no links or files are given, but the integration's `payload` names a store, the run stops at once with a message that says what to set (nothing charged).

### Pricing

This Actor uses pay-per-event pricing. The current prices are on the **Pricing** tab of this Actor.

| Event | Price | When it is charged |
|---|---|---|
| Actor start | $0.00005 per GB of memory | Set by Apify: once per GB of the run's memory, at least once ($0.0001 with the default 2 GB), also for the built-in sample. |
| Frame extracted (`frame-extracted`) | $0.0015 per event (= $1.50 per 1,000) | **One per frame saved.** The decoding and the video data of a video are counted with the same event, see below. |

What one video counts, in `frame-extracted` events:

- **Frames**: one per saved frame.
- **Decoding**: to find your frames, ffmpeg has to decode the video. Each saved frame includes 400 megapixels of decoding (about 6.4 seconds of 1080p video at 30 fps, or 1.6 seconds of 4K). Decoding beyond that counts one more event per 400 megapixels. So a video counts *the larger of its frames and its decoded megapixels / 400*.
  - Evenly spaced frames and frames every N seconds jump to the keyframe just before each time and decode only from there. In normal videos (a keyframe every few seconds) that adds nothing.
  - Scene changes and keyframes decode the whole part of the video you chose. So do evenly spaced frames in a video with very few keyframes (typical for screen and browser recordings): there, jumping would decode the video again and again, so the Actor reads it once from start to end instead.
  - **A video never counts more decoding than one pass over the part you chose.** For example, one minute of 1080p at 30 fps is 1,800 frames x 2.07 MP = 3,732 MP, so it counts at most 9.3 events for decoding: scene detection that finds 3 scenes in it counts 9.3 events in total (not 3 + 9.3), and finding 20 scenes counts 20.
- **Video data**: one event per 64 MB downloaded from a link, or per 16 MB read from your Apify storage (key-value store or upload; Apify charges for reading storage).
- The decoding and data parts are fractions per video. They are added up over the run and **rounded up once at the end of the run**, so many small videos are not rounded up one by one. Before each video the log shows the events it is expected to count.

Examples:

| Job | Events | Price |
|---|---|---|
| 10 thumbnails from a 100 MB 1080p video at a link | 10 + 1.6 (data), rounded up: 12 | $0.018 |
| The same from your key-value store | 10 + 6.25 (data), rounded up: 17 | $0.0255 |
| 1 thumbnail from a 2 GB video at a link | 1 + 33.6 (data), rounded up: 35 | $0.0525 |
| 6 frames from a 2 MB clip at a link | 6 + 0.03, rounded up: 7 | $0.0105 |
| Scene changes in a 10-minute 1080p30 video (300 MB in your store), 30 scenes found | the larger of 30 and 93.3 (decoding), + 18.75 (data), rounded up: 113 | $0.1695 |
| 10 evenly spaced frames from a 30-minute 1080p30 screen recording with one keyframe (300 MB at a link) | read in one pass up to the last frame: the larger of 10 and 266 (decoding), + 4.7 (data), rounded up: 271 | $0.4065 |
| The built-in sample | 0 | $0 |

The prices above are for the `frame-extracted` events; the Actor start event ($0.0001 with the default 2 GB) is added once per run.

- Each frame record shows its `chargedEvents` (1); the summary shows what was counted (`events`) and what was really charged (`chargedEvents`), and each video its `extraEvents` and `decodePlan`.
- Skipped records and the built-in sample have no `frame-extracted` events. A video that fails before any frame is saved is not charged. A video that fails after some frames were saved (for example the file breaks off half-way) is charged for those saved frames only. The decoding and data events of a video are counted only when that video is finished; a video stopped part-way is charged only for its saved frames.
- A run stopped by an input check (wrong values, a store given only in `payload`, no videos found, every video failed) has no `frame-extracted` events.
- **No double charges**: frames are charged in small batches right after their records and the progress are saved. If Apify moves the run to another server, or you resurrect a run that failed or was stopped from outside (for example out of memory), it continues after the last saved frame, and frames already charged are not charged again. After such a restart a few frame records can appear twice in the dataset (the same `imageKey`); they are charged once. A run that ended with a `PARTIAL` summary (timeout, abort, maximum cost) is finished: start a new run for the rest. In the worst case one batch that was being saved when the run stopped is not charged.
- **Abort and timeout**: the Actor stops 30 seconds before the run's timeout, and at once when you abort, records and charges the frames already saved, and writes a `PARTIAL` summary. A running download or ffmpeg is stopped at once. A whole-video pass (scenes, keyframes, or one pass for few keyframes) that would take more than twice the time left (estimated from speeds measured on Apify, with a margin) is not started: that video fails with `NOT_ENOUGH_TIME` (not charged) and the run goes on. When the timeout is too close to start any video, the summary says so.
- If a run is stopped in a way the Actor cannot see (for example out of memory), the summary keeps the last progress (`"status": "RUNNING"`).
- **Maximum cost per run**: before each video the Actor checks that your limit covers one frame plus the video's expected data and decoding events; for a store record or a link that sends its size, this is checked before the file is read. Before each frame it checks that frame. When the limit does not cover them, the Actor stops cleanly: the frames so far are saved and the summary says `"status": "PARTIAL"`. If a video decodes more than expected, its extra events are charged only as far as your limit allows; the limit is never exceeded.
- Memory: the default 2 GB fits videos up to 4K and gives the run 4 GB of disk for the downloaded file. More memory gives a faster run (1 CPU core per 4 GB) and more disk, but the Actor start event is charged once per GB.

### Limits

- **File size**: up to 2 GB per video, and at most the free disk of the run (Apify gives a run twice its memory as disk: 4 GB with the default 2 GB, minus what the system uses). A bigger file fails with `FILE_TOO_LARGE` and a message that says how much memory to use; nothing is charged. A link whose `Content-Length` is too big is refused before its body is downloaded.
- **Downloads**: one download may take at most 10 minutes (and must end before the run's timeout), the server must answer within 30 seconds and must not pause for more than 30 seconds, and after the first minute the download must average at least 1 MB/s. Otherwise the video fails with `TIMEOUT`, `DOWNLOAD_FAILED` or `SLOW_DOWNLOAD` and is not charged. For a file on a slow host, put it in an Apify key-value store (and choose that store) or upload it. The same link given twice is downloaded once.
- **Frames**: at most 500 per video (and at most "Maximum frames per video", default 50); output images at most 3840 px wide and 3840 x 2160 pixels in total. When fewer frames come out than you asked for, the video's `note` in `OUTPUT.videos` says why (the maximum, the end of the video track, or times closer than one frame), and for frames every N seconds which part of the video they cover.
- **Formats**: MP4, MOV, WebM, MKV and AVI containers with the video codecs of ffmpeg (for example H.264, HEVC/H.265, VP8, VP9, AV1, MPEG-4, ProRes, MJPEG). Not supported: MPEG transport streams (`.ts`), FLV, HLS/DASH playlists, DRM-protected video, and links to video pages or streaming sites.
- **Picture**: interlaced video is deinterlaced. HDR video (for example from recent phones) is converted without tone mapping, so the frames' colors can look pale; such a video gets a note.
- **Length**: the length of the video track is used, so a longer sound track does not add times without frames.
- **Memory**: before each video the Actor checks that decoding and the images fit into what is left of the run's memory. Videos up to 4K fit 1 GB; 8K videos fit 1 GB only with frames up to about 1280 px wide, and the default 2 GB at any frame size. A video that does not fit fails with `TOO_LITTLE_MEMORY` and the memory it needs (not charged).
- **Speed, measured on our test computer** with one decoding thread (about the CPU of a 4 GB Apify run; synthetic test videos with noise, which decode slower than most real footage):

| Test | Time for the whole video |
|---|---|
| Built-in sample, 6 frames + contact sheet | about 1 s |
| 1080p H.264, 60 s, 12 Mbps (90 MB): 10 evenly spaced frames | 5.0 s |
| ... 100 evenly spaced frames (read in one pass) | 13.6 s |
| ... one frame every 10 s (6 frames) | 2.5 s |
| ... scene changes (all 1,800 frames decoded) | 12.6 s |
| ... keyframes (38 keyframes decoded) | 3.3 s |
| 1080p H.264, 30 s, one keyframe: 10 evenly spaced frames (read in one pass) | 6.5 s |
| 4K HEVC, 10 s (38 MB): 10 evenly spaced frames | 8.4 s |
| ... scene changes (300 frames decoded) | 8.4 s |

Peak memory in these tests: ffmpeg 48-193 MB, the Actor's own process 92-223 MB.

- **Measured on Apify** (synthetic test videos, whole runs):

| Test | Memory | Time | Peak memory |
|---|---|---|---|
| Built-in sample | 2 GB | 4.6-8.8 s | 96 MB |
| 1080p H.264 60 s (90 MB in your store): 10 evenly spaced frames | 2 GB | 8-10 s | 213 MB |
| ... 100 evenly spaced frames (one pass) | 2 GB | 59 s | - |
| ... scene changes (all 1,800 frames decoded) | 2 GB | 31 s | - |
| 1080p, 30 s, one keyframe: 10 evenly spaced frames (one pass) | 2 GB | 8.7 s | - |
| 4K HEVC 10 s: 10 evenly spaced frames, 3840 px wide | 1 GB | 24 s | 466 MB |
| 1080p, 3 minutes with a cut every 5 s: scene changes | 1 GB | about 100 s | - |

Scene detection (and any whole-video pass) on Apify decodes about 30-35 seconds per minute of 1080p video, at 1 GB and at 2 GB alike: set the run's timeout (Run options) to cover your longest video.

- **Interruptions**: a video stopped part-way (timeout, abort, maximum cost) is charged only for its saved frames; the decoding and data events of that video are not charged. After a migration or a resurrect, the video is decoded again from its start and its decoding is counted once, when it is finished.
- Videos are handled one at a time.
- The Actor only reads the links you give, the key-value store you choose and the files you upload (it runs with limited permissions).

### Privacy

- The Actor downloads only the links you give (one request each) and reads only the key-value store you choose and the files you upload. It does not send your videos or frames to any other service.
- Each video is kept on the run's temporary disk only while it is processed, and deleted afterwards. The frames and contact sheets are saved in this run's default key-value store in your account, like every Actor's results, and are kept or deleted according to your account's data retention.
- The dataset, the summary and the log show links without their query string (signed links can carry tokens there), file names, sizes, times and video facts. The frames are written without metadata.
- Everything visible in the video (faces, text, licence plates) is in the frames. Only use videos you have the right to use.

### More from Quiet Works Lab

- [Image Converter & Compressor](https://apify.com/quietworkslab/image-converter): convert frames or any images to WebP or AVIF and remove EXIF and GPS data.
- [Dataset Change Tracker](https://apify.com/quietworkslab/dataset-change-tracker): compare two runs of your scraper and get only the new, changed and removed items.
- [Japanese Text Normalizer & RAG Chunker](https://apify.com/quietworkslab/japanese-text-normalizer): clean Japanese text before an LLM or RAG and split it into sentence-aware chunks.

### Example tasks

Ready-made inputs for common jobs. Open one, then click **Try for free** to copy it to your account and add your videos:

- [Extract evenly spaced thumbnails from a video](https://apify.com/quietworkslab/video-frame-extractor/examples/extract-thumbnails-from-video)
- [Detect scene changes and save one frame per shot](https://apify.com/quietworkslab/video-frame-extractor/examples/detect-scene-changes-in-video)
- [Save one frame every N seconds from a video](https://apify.com/quietworkslab/video-frame-extractor/examples/extract-frame-every-n-seconds)
- [Make a contact sheet (thumbnail grid) of a video](https://apify.com/quietworkslab/video-frame-extractor/examples/video-contact-sheet)
- [動画から等間隔のサムネイル画像を切り出す](https://apify.com/quietworkslab/video-frame-extractor/examples/video-thumbnail-images-japanese)

### More from Quiet Works Lab

- [Image Converter & Compressor](https://apify.com/quietworkslab/image-converter): convert images to WebP, AVIF, JPEG or PNG, resize them and remove EXIF and GPS metadata.
- [Merge, Deduplicate & Join Datasets](https://apify.com/quietworkslab/merge-dedup-datasets): merge your datasets, remove duplicates by key and join fields from a lookup dataset.
- [Dataset Change Tracker](https://apify.com/quietworkslab/dataset-change-tracker): compare two runs of your scraper and get only the new, changed and removed items.
- [Japanese Text Normalizer & RAG Chunker](https://apify.com/quietworkslab/japanese-text-normalizer): clean Japanese text before an LLM or RAG and split it into sentence-aware chunks.

### Support

This Actor is developed and maintained with the help of AI (Claude Code). Issues are answered within a few days.

Please open an issue on the **Issues** tab with the run ID and, if you can, a short sample video that shows the problem (remove anything private first).

# Changelog

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

# Actor input Schema

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

Direct links to video files (MP4, MOV, WebM, MKV or AVI) that you have the right to use, one per line, for example https://example.com/clip.mp4. Not web pages or streaming sites: the link must return the file itself. Each link is downloaded once (up to 2 GB and the run's free disk, within 10 minutes and at least 1 MB/s on average; for slower hosts, upload the file or use a key-value store). Besides one event per frame, each downloaded file counts one frame-extracted event per 64 MB (a 100 MB file adds 1.6, see Pricing). Links that need a login, and links to private or local network addresses, are refused. Leave empty (with no store and no uploads) to try the Actor on the built-in sample clip.

## `keyValueStoreId` (type: `string`):

Optional. A key-value store in your account that holds videos. Every record that is a video is handled; JSON, text and image records are skipped for free. Besides one event per frame, each video read from your storage counts one frame-extracted event per 16 MB (Apify charges for reading storage; a 100 MB video adds 6.25, see Pricing). When this Actor runs after another Actor from its Integrations tab, set it to {{resource.defaultKeyValueStoreId}}. Uploaded files that are in this store are handled with it, once.

## `keyPrefix` (type: `string`):

Optional. Handle only the records of the key-value store whose key starts with this text, for example clip- or 2026-10-. Empty = all records.

## `uploadedFiles` (type: `array`):

Optional. Upload video files here (MP4, MOV, WebM, MKV or AVI). The files are stored in a key-value store of your Apify account and read from there, so they count like store records: one frame-extracted event per 16 MB on top of the frames. If your account keeps its storages private (the default), this Actor, which runs with limited permissions, may not open the uploads: the run then names the store they are in. Choose that store in "Key-value store" and start again.

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

count: "Number of frames" evenly spaced over the video (thumbnails, previews). interval: one frame every "Seconds between frames". scenes: the first frame and every frame where the picture changes a lot (shot cuts); the whole video is decoded, which takes longer and can count extra events for long videos with few scenes (see Pricing). keyframes: the video's own keyframes, spread over the video. In count and interval mode a video with very few keyframes (typical for screen and browser recordings) is also decoded once from start to end, which can count extra events like scenes (see Pricing).

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

For mode count: how many evenly spaced frames per video, each in the middle of one of that many equal parts of the video, at most "Maximum frames per video" (default 50: raise it for more). Empty = 10 (6 for the built-in sample clip).

## `intervalSec` (type: `number`):

For mode interval: one frame at the start and then one every this many seconds, for example 5. At most "Maximum frames per video": if that stops it early, the summary says which part of the video the frames cover.

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

For mode scenes: how different a frame must be from the previous one to count as a new scene, from 0.01 (many frames) to 1 (only total changes). 0.3 finds most shot cuts; try 0.2 for soft cuts or 0.4 for fewer frames.

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

Upper limit of frames per video in every mode. In scenes and keyframes modes the first frames found are kept (keyframes are spread over the video first).

## `startSec` (type: `number`):

Optional. Ignore the video before this time, for example 12.5 to skip an intro. Empty = from the beginning.

## `endSec` (type: `number`):

Optional. Ignore the video after this time; must be later than "Start at". Empty = to the end.

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

Format of the extracted frames and of the contact sheets.

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

Frames wider than this are shrunk, keeping the aspect ratio, for example 640 for thumbnails or 3840 for full 4K. Frames are never enlarged. Rotated phone videos are turned upright first.

## `quality` (type: `integer`):

Compression quality from 1 (smallest file) to 100 (best) for WebP and JPEG. PNG is always lossless.

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

Also save one grid image per video with its frames and their times (up to 100 tiles). Not charged.

## Actor input object example

```json
{
  "mode": "count",
  "intervalSec": 10,
  "sceneThreshold": 0.3,
  "maxFramesPerVideo": 50,
  "format": "webp",
  "maxWidth": 1280,
  "quality": 80,
  "contactSheet": true
}
```

# Actor output Schema

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

One record per frame: video, frame number, time in seconds and as a timecode, image size, link to the image, scene score. A video that failed has one record with the error.

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

List of the records in the run's key-value store: the frame images (for example 001-intro-0003.webp), one contact sheet per video (001-intro-contact-sheet.webp) and OUTPUT.

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

Status, settings, counts, events and, for each video, its length, frame rate, codec, resolution and contact-sheet key (record OUTPUT). It is written at the start and during the run, so it also exists when a run is stopped (status RUNNING).

# 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("quietworkslab/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("quietworkslab/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 quietworkslab/video-frame-extractor --silent --output-dataset

```

## MCP server setup

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