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

- **URL**: https://apify.com/quietworkslab/video-frame-extractor/changelog.md
- **Full Actor documentation**: https://apify.com/quietworkslab/video-frame-extractor.md

## Changelog

### 0.1 (published on Apify Store on 2026-11-01)

- First version: extract frames from videos given as direct file links, in a key-value store (optionally only keys with a prefix) or as uploaded files. MP4, MOV, WebM, MKV and AVI, detected by their first bytes.
- Four modes: a number of evenly spaced frames, one frame every N seconds (both by accurate seeking), scene changes (with a threshold) and keyframes. Optional start and end time, at most 500 frames per video.
- Frames as WebP, JPEG or PNG, turned upright, square pixels, shrunk to a maximum width (never enlarged), without metadata, with the exact time of each decoded frame. One contact sheet per video with the frame times.
- Video links (CONVENTIONS §11): one GET per link, up to 5 redirects checked one by one, retries only after 5xx or a broken connection, HTML pages and other types refused by Content-Type and by the first bytes, the size limit counted while streaming to disk, private/local/reserved addresses refused after DNS and on every redirect (the connection goes to the checked address), only a User-Agent header, no cookies or logins.
- Per-video facts (length, frame rate, codec, resolution, contact sheet) in OUTPUT.
- Pay-per-event: `frame-extracted`, one per saved frame; decoding beyond 400 MP per frame and the video data read (4 MB per event from links, 16 MB from Apify storage) are counted with the same event and rounded up once per run. Failed videos and the built-in sample are not charged. Stops cleanly at the maximum cost per run; frames are charged in small batches right after they are saved, never twice after a migration.
- After the independent review and the first checks on Apify (2026-11-01, after build 0.1.2):
  - Links: a refused answer (an error, a web page, a file over the size limit) is closed at once instead of being read to its end in the background. A download may take at most 10 minutes, must answer within 30 s, and must average 1 MB/s after its first minute (`SLOW_DOWNLOAD`, which suggests a key-value store or an upload instead). The time limit also ends the waits between retries; the next checked address is tried when a connection fails; IPv4-translated IPv6 addresses (`::ffff:0:a.b.c.d`) are refused; the same link given twice is downloaded once.
  - Data from links counts one event per 64 MB instead of 4 MB (downloads are not billed as data transfer, measured on Apify). Store and upload reads stay at 16 MB.
  - Decoding is never counted for more than one pass over the chosen part of a video. Evenly spaced frames and frames every N seconds read the keyframe positions first; in videos with very few keyframes (screen and browser recordings) they are taken in one pass instead of one seek each (faster). `OUTPUT.videos[].decodePlan` says which.
  - The budget is checked before a file is read (store size, Content-Length); while a charge waits, the next store record is only peeked at. A whole-video pass that cannot finish anywhere near the time left fails that video with `NOT_ENOUGH_TIME` (not charged).
  - A resurrected FAILED run continues after its saved frames instead of charging them again.
  - The length is taken from the video track (a longer sound track no longer adds times without frames). Fewer frames than asked (the maximum per video, the end of the track, frames closer than one frame), HDR video and frames saved before a failure are explained in the video's `note`; interval mode says which part the frames cover. Interlaced video is deinterlaced. The log shows the expected events of each video.
  - ffmpeg and ffprobe open only local files, with the demuxer of the detected container; the Docker build checks for ffmpeg 5.1 or later.
  - OUTPUT's `mode` field is now `source` (where the videos came from); the extraction mode is `settings.mode`. A run where only servers failed says "No video could be used" instead of "Input problem".
- After the second review and the checks of build 0.1.3 on Apify (2026-11-01):
  - The one pass keeps exactly the frames seeking keeps: a frame is kept when a requested time falls after the previous frame and at or before it, so times that land on the same frame give it once. Before, in VFR video with a gap (a screen recording that stood still) or at a low frame rate, the pass took the next frames instead (more frames, charged, than asked for by time). Times are compared with a 1 microsecond tolerance, so a frame exactly at a time is no longer skipped.
  - The note of interval mode gives the times of the frames really saved.
  - The time estimate of a whole-video pass is calibrated on Apify (about 0.009 s per decoded megapixel at 1 GB and 2 GB, plus 50%); a pass is refused (`NOT_ENOUGH_TIME`) only above twice the time left (was three times a 3x too pessimistic estimate).
  - When the timeout is too close to start any video, the summary says so. Messages use the singular for one (1 frame, 1 video). A run where only servers failed suggests a new run instead of a resurrect.
- After the third review (2026-11-01): a video whose scan or seek is stopped by the run's timeout is now reported as stopped part-way (PARTIAL), not as failed with "Decoding took longer than ..." (the run's stop signal and ffmpeg's own time limit had the same name). The one pass compares times at full precision, so at 30 or 29.97 fps a time exactly on a frame keeps the same frame as a seek. A timeout before the first frame of a started video has its own message. README: the prices are in the tables ($0.0015 per frame-extracted event, $0.00005 per GB for the Actor start).
- Downloads, after the review of the same code in Image Converter (2026-11-01): an answer that never becomes a response (101) used to hang the download for ever, past the 10-minute limit and past an abort; it now fails at once, and the time limit and stop signals hold whatever a download waits for. A body with neither a `Content-Length` nor chunks ends when the connection does, so a video cut by the time limit, a stop or the server looked whole, and the frames of the part that had arrived were charged (MP4 with its index in front, WebM, MKV, AVI). Now the time limit and a stop are seen first, and a body like that must reach the end that its own container declares, when it declares one; otherwise it fails with `DOWNLOAD_FAILED` and is not charged. A file that does not say, such as an AVI written to a pipe or a live MKV, and data after the last box of an MP4 or MOV, are read as before. After a silent address one more address of the host is tried, in the same attempt. Messages about a link or an upload that is not a valid URL no longer repeat that text, because it can hold a password or a token.
