Video Frame Extractor - Thumbnails, Scenes & Contact Sheet avatar

Video Frame Extractor - Thumbnails, Scenes & Contact Sheet

Pricing

from $1.05 / 1,000 frame extracteds

Go to Apify Store
Video Frame Extractor - Thumbnails, Scenes & Contact Sheet

Video Frame Extractor - Thumbnails, Scenes & Contact Sheet

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.

Pricing

from $1.05 / 1,000 frame extracteds

Rating

0.0

(0)

Developer

Quiet Works Lab

Quiet Works Lab

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Security

Limited permissions

Share

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), 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

FieldWhat it does
videoUrlsDirect 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.
keyValueStoreIdKey-value store in your account that holds videos. Each video also counts one event per 16 MB read.
keyPrefixOnly handle records whose key starts with this text, for example clip-.
uploadedFilesFiles uploaded in the input form. See Uploaded files.
modecount (default), interval, scenes or keyframes.
frameCountcount mode: frames per video, at most maxFramesPerVideo. Empty = 10 (6 for the built-in sample).
intervalSecinterval mode: seconds between frames, default 10.
sceneThresholdscenes mode: 0.01-1, default 0.3. Lower finds more (also soft) changes, higher only hard cuts.
maxFramesPerVideoUpper limit per video in every mode, default 50, at most 500.
startSec, endSecOptional part of the video to use.
formatwebp (default), jpeg or png.
maxWidthDefault 1280, 32-3840. Frames are never enlarged.
quality1-100, default 80 (WebP and JPEG).
contactSheetDefault true: one grid image per video. Not charged.

Example input:

{
"videoUrls": ["https://example.com/videos/product-demo.mp4"],
"mode": "scenes",
"sceneThreshold": 0.3,
"maxFramesPerVideo": 40,
"format": "webp",
"maxWidth": 1280,
"contactSheet": true
}
  • 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:

{
"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:

{
"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:
{
"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.

EventPriceWhen it is charged
Actor start$0.00005 per GB of memorySet 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:

JobEventsPrice
10 thumbnails from a 100 MB 1080p video at a link10 + 1.6 (data), rounded up: 12$0.018
The same from your key-value store10 + 6.25 (data), rounded up: 17$0.0255
1 thumbnail from a 2 GB video at a link1 + 33.6 (data), rounded up: 35$0.0525
6 frames from a 2 MB clip at a link6 + 0.03, rounded up: 7$0.0105
Scene changes in a 10-minute 1080p30 video (300 MB in your store), 30 scenes foundthe 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 sample0$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):
TestTime for the whole video
Built-in sample, 6 frames + contact sheetabout 1 s
1080p H.264, 60 s, 12 Mbps (90 MB): 10 evenly spaced frames5.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 frames8.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):
TestMemoryTimePeak memory
Built-in sample2 GB4.6-8.8 s96 MB
1080p H.264 60 s (90 MB in your store): 10 evenly spaced frames2 GB8-10 s213 MB
... 100 evenly spaced frames (one pass)2 GB59 s-
... scene changes (all 1,800 frames decoded)2 GB31 s-
1080p, 30 s, one keyframe: 10 evenly spaced frames (one pass)2 GB8.7 s-
4K HEVC 10 s: 10 evenly spaced frames, 3840 px wide1 GB24 s466 MB
1080p, 3 minutes with a cut every 5 s: scene changes1 GBabout 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

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:

More from Quiet Works Lab

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).