Video Frame Extractor - Screenshots, Scenes, Contact Sheet
Pricing
from $4.00 / 1,000 video processeds
Video Frame Extractor - Screenshots, Scenes, Contact Sheet
Extract frames from video URLs (mp4, webm, mov, mkv, m3u8) with FFmpeg: every N seconds, N evenly spaced, scene changes or exact timestamps. JPG/PNG/WebP, public links, optional contact sheet. Pay per video and frame.
Pricing
from $4.00 / 1,000 video processeds
Rating
0.0
(0)
Developer
Andrew Boiko
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
28 minutes ago
Last modified
Categories
Share
Video Frame Extractor - Screenshots, Scene Changes and Contact Sheets (FFmpeg)
Turn video links into image files. Give the Actor direct video URLs (MP4, WebM, MOV, MKV or an HLS .m3u8 playlist) and it extracts frames with FFmpeg: every N seconds, N evenly spaced frames, frames at detected scene changes, or frames at exact timestamps. Every frame is stored as JPG, PNG or WebP with a public link, and you can get one contact sheet per video.
Pay only for videos that were processed and frames that were stored. Unreadable videos, HTTP errors and anything over your limits are never charged. New Apify accounts can try it with the free plan credit.
Who is it for
- Teams that need thumbnails or preview stills for a video library.
- Content, SEO and social teams creating cover images, storyboards or slide-style summaries.
- Developers feeding frames into OCR, image classification or vision-model pipelines (the dataset gives you stable image URLs and timestamps).
- QA and compliance teams that need periodic snapshots of a video, or the frame at a known time code.
What it does and does not do
- Reads the video over HTTP with range requests: for interval, count and timestamp modes FFmpeg seeks to each position and only reads the part of the file it needs, it does not download the whole video.
scenesmode must decode the whole video once. - Needs a direct video file or HLS playlist URL. Pages such as YouTube, Vimeo, TikTok or Instagram are not video files and will fail with a clear error. DRM-protected or encrypted streams are not supported. Live streams without a known duration only work in
timestampsmode. - Frames are exact to the video's frame grid at the requested time. Scene detection runs at 5 frames per second on a 320 px wide copy, so scene times are accurate to about 0.2 s.
- No audio extraction, no video clips, no OCR and no face detection.
Input
| Field | Type | Default | Description |
|---|---|---|---|
videoUrls | array | required (or videoFiles) | Direct video links, one per row. |
videoFiles | array | [] | Videos stored in an Apify key-value store: storeId/recordKey, read with the run's token. |
mode | interval, count, scenes, timestamps | count | How frame positions are chosen. |
intervalSeconds | integer | 10 | Mode interval: seconds between frames, starting at 0. |
frameCount | integer 1-300 | 8 | Mode count: evenly spaced frames, centered in equal slices. |
sceneThreshold | string "0.1".."0.9" | "0.3" | Mode scenes: lower finds more (soft) cuts, higher only hard cuts. |
timestamps | array | [] | Mode timestamps: 83.5, 01:23, 00:01:23.500, 90s. |
maxFramesPerVideo | integer 1-300 | 20 | Hard cap per video (protects your budget). |
format | jpg, png, webp | jpg | Image format. |
maxWidth | integer 0-7680 | 1280 | Scale down wider frames (never up). 0 keeps the original size. |
quality | integer 1-100 | 85 | JPG/WebP quality. |
makeContactSheet | boolean | false | One tiled JPG per video. Included in the price. |
contactSheetColumns | integer 1-12 | 5 | Grid columns of the contact sheet (320 px tiles). |
maxDurationMinutes | integer | 120 | Longer videos are rejected without charge. |
maxVideoSizeMb | integer | 2048 | Files larger than this (as reported by the server) are rejected without charge. |
includeFailed | boolean | false | Add free rows with status: "failed" for videos that failed. |
Output
The dataset has one row per frame (type: "frame") and one summary row per video (type: "video"). Frame images are in the default key-value store; frameUrl is the public link.
Frame row:
{"type": "frame","id": "41be81f899:2","videoId": "41be81f899","videoUrl": "https://example.com/video.mp4","frameIndex": 2,"timestampSec": 4.5,"timestampLabel": "00:00:04.500","frameUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/frames-41be81f8-0002.jpg","recordKey": "frames-41be81f8-0002.jpg","format": "jpg","width": 1280,"height": 720,"bytes": 84231,"sceneScore": null,"extractedAt": "2026-10-14T10:57:10.203Z","source": "ffmpeg"}
Video row:
{"type": "video","id": "41be81f899","videoUrl": "https://example.com/video.mp4","status": "ok","mode": "count","durationSec": 12,"resolution": "1920x1080","width": 1920,"height": 1080,"fps": 29.97,"codec": "h264","container": "mov","bitrate": 3439000,"hasAudio": true,"frames": 4,"contactSheetUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/contact-sheet-41be81f8.jpg","warnings": [],"processedAt": "2026-10-14T10:57:10.221Z","source": "ffmpeg"}
frameIndex starts at 1. In scenes mode sceneScore holds FFmpeg's scene change score (0-1) for that frame. Record keys use frames-<videoHash>-<n>.<ext> (key-value store keys cannot contain slashes).
The run also writes OUTPUT (counts, charged events, failed videos with reasons, warnings, stop reason) and a short RESULTS_MD to the key-value store.
Pricing
Pay-per-event, pay only for videos processed and frames stored:
| Event | Price | When |
|---|---|---|
video | $0.005 | One video successfully opened, probed and processed with at least one frame stored. |
frame | $0.001 | One frame image extracted and stored. |
Example: 3 frames from one video cost $0.005 + 3 x $0.001 = $0.008. 100 frames from 5 videos cost $0.025 + $0.10 = $0.125.
Not charged: videos that cannot be opened, are not videos, are too long or too large, frames that FFmpeg could not produce, free includeFailed rows, and anything beyond your maximum cost per run (the run then stops cleanly and OUTPUT.stoppedReason is budget). The contact sheet is free. Apify may apply Bronze/Silver/Gold plan discounts.
How it works
- Each URL is checked (public address only, redirects followed with the same check) and its size is read with a HEAD request.
ffprobereads duration, resolution, frame rate and codec.- Frame positions are planned for the chosen mode.
scenesmode first runs one analysis pass with FFmpeg's scene filter. - FFmpeg extracts each frame with a fast input seek (
-ssbefore-i), a few at a time per host. - Frames are saved to the key-value store, then charged and added to the dataset one by one.
- Progress is saved after every frame. After a migration or restart the Actor continues and does not charge a video or frame twice.
Limits and honest caveats
- The video host must answer from a normal HTTP(S) server. Signed URLs that expire, hosts that block data-center IPs and pages that need cookies or logins cannot be read.
- Seeking is fast for MP4 with the index (
moov) at the start and for HLS. MP4 files with the index at the end, or very sparse keyframes, are slower. scenesmode reads and decodes the entire video; a 2-hour 1080p video can take many minutes. UsemaxDurationMinutesto bound it.- If no scene change reaches the threshold, one frame from the middle of the video is returned with a warning (lower
sceneThresholdfor more). - Frames at the very end of a video are clamped slightly before the last frame.
- Frames are not de-duplicated: if you ask for 10 frames of a still image you get 10 near-identical images.
Legal and ethical notes
- You are responsible for having the right to process the videos you submit. The Actor does not bypass access controls or DRM.
- Frames of people are personal data under GDPR and similar laws; make sure you have a lawful basis.
- Extracted frames are stored in your own Apify key-value store under its retention rules. Video URLs are only contacted to read the video.
- FFmpeg is used as a separate program (
ffmpeg-staticandffprobe-staticbinaries). FFmpeg is licensed under the GPL/LGPL; see the FFmpeg project for details.
FAQ
Can I use YouTube links? No. YouTube pages are not direct video files, and downloading them may violate YouTube's terms. Use a direct file or playlist URL you are allowed to access.
Does it download the whole video?
Not in interval, count and timestamps modes: it seeks with range requests. scenes mode reads the whole file.
What does a frame cost? $0.001 per frame plus $0.005 per video.
How many frames can I get from one video?
Up to 300 per video (maxFramesPerVideo), set by you.
Can I get the frame at an exact time?
Yes, use mode timestamps with values like 00:01:23.500.
What happens if my budget runs out mid-run?
The Actor stops cleanly, keeps the frames already delivered and lists the rest in OUTPUT. Uncharged frames are deleted from the store.
Is the order of dataset rows guaranteed? Frame rows of a video come first in time order, then the video summary row. With several videos the rows of different videos can interleave.
Which formats are supported? Anything FFmpeg can decode over HTTP: H.264, HEVC, VP8/VP9, AV1 and more, in MP4, MOV, WebM, MKV, TS or HLS.
Use via API
Replace username~video-frame-extractor with this Actor's ID and YOUR_TOKEN with your Apify API token.
curl -X POST "https://api.apify.com/v2/acts/username~video-frame-extractor/run-sync-get-dataset-items?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"videoUrls":["https://example.com/video.mp4"],"mode":"count","frameCount":6,"format":"jpg"}'
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_TOKEN' });const run = await client.actor('username~video-frame-extractor').call({videoUrls: ['https://example.com/video.mp4'],mode: 'interval',intervalSeconds: 10,});const { items } = await client.dataset(run.defaultDatasetId).listItems();const frames = items.filter((x) => x.type === 'frame');console.log(frames.map((f) => f.frameUrl));
from apify_client import ApifyClientclient = ApifyClient("YOUR_TOKEN")run = client.actor("username~video-frame-extractor").call(run_input={"videoUrls": ["https://example.com/video.mp4"],"mode": "scenes","sceneThreshold": "0.3",})for item in client.dataset(run["defaultDatasetId"]).iterate_items():if item["type"] == "frame":print(item["timestampLabel"], item["frameUrl"])
Integrations
Connect the Actor to Make, n8n, Zapier or Google Sheets with the Apify integrations: start a run with a list of video URLs, then use the frameUrl column to save images to Drive, a CMS or a vision-model step. Webhooks tell your system when a run finishes.
Support
Open an issue from the Actor page with the run link and the video URL (if it is public). Please do not send videos you do not have rights to.
More tools by the same author
- Website Screenshot API: Full-page and mobile screenshots in bulk
- Image OCR Bulk: Bulk OCR for images
- Bulk Image and File Downloader: Bulk download of images and files
- PDF Text and Table Extractor: PDF to text, tables and Markdown with OCR