Method guide
Count, interval, keyframe, timestamp: what the four modes actually do
· 15 min read
A frame extractor asks the browser for a moment and then photographs whatever is on screen. Both halves of that sentence hide detail. Which moments get requested depends on the mode; what appears on screen depends on how the decoder handles a seek. Get either one wrong and you end up with a still that is a tenth of a second away from the event you were documenting, with a filename that says otherwise.
This page states the arithmetic each mode uses, explains why the browser cannot always honour the request exactly, and gives a way to measure the difference on your own footage using a barcode burned into every frame. It also publishes what happened when we ran that measurement here: 34 seeks across the four modes, in one browser on one machine, against a file whose frames each carry their own index. The scope travels with the numbers, because a drift figure without a scope is the kind of fact that gets quoted long after the machine it came from is gone.
In short
- Count mode requests N evenly spaced points at k × duration ÷ (N + 1). It never requests the first or last instant, which is deliberate.
- Interval mode starts at zero and steps forward, so it always includes the opening frame and silently drops the final partial segment.
- Keyframe mode steps on a fixed two-second grid. It is an estimate of where keyframes probably are, not a read of the container’s keyframe index.
- A seek is not frame-exact. The browser decodes forward from the nearest earlier keyframe, and the frame you receive covers a time interval rather than an instant.
- In the measurement published here, the decoder landed on the exact expected frame in 34 of 34 samples — so the error that matters comes from the arithmetic before the seek, not from the seek itself.
Contents
- 1. Four modes answer four different questions
- 2. Count mode: why the first and last frame are missing
- 3. Interval mode: the trailing segment disappears
- 4. Keyframe mode is a two-second grid, and it says so here
- 5. What actually happens when you ask for 12.500 seconds
- 6. Measure the drift on your own file
- 7. What the output format does to the pixels
- 8. Choose by the cost of being wrong
Four modes answer four different questions
The modes are not three ways to do the same thing with different settings. They answer different questions, and choosing the wrong one produces a set of stills that is complete in a way you did not want.
| Mode | What you supply | What it requests | Best for | Characteristic failure |
|---|---|---|---|---|
| Count | A number, N | k × D ÷ (N + 1) for k = 1…N | An overview of a clip you have not watched yet | Misses a short event that falls between two samples |
| Interval | Seconds, s | 0, s, 2s, 3s… while t < D | A regular visual log where spacing is the point | Produces many near-identical frames on static footage |
| Keyframes | Nothing | 0, 2, 4, 6… while t < D | A rough first pass over a long clip | The name promises more precision than the implementation delivers |
| Timestamps | An explicit list | Exactly those times, clamped to the clip | Evidence, QA, anything where the moment is known | Only as good as the notes you are reading them from |
The pattern is that count and interval distribute coverage, while timestamps concentrate it. If you know when the thing happened, timestamps is not merely the better choice — it is the only one that can be right. If you do not know, distributing coverage is the only honest option, and the question becomes how to distribute it so the event cannot hide between samples.
Count mode: why the first and last frame are missing
Count mode divides the duration by N + 1 and requests the N interior points. For a 60-second clip and N = 5, the divisor is 6, so the requested times are 10, 20, 30, 40 and 50 seconds.
Dividing by N + 1 rather than N − 1 is a small decision with a visible consequence: 0:00 and 0:59.9 are never requested. That is usually right. The first and last frames of an export are disproportionately likely to be a fade from black, a title card, or a frame the editor trimmed to the boundary, and including them makes every overview look like it starts with a black rectangle. But it is a decision, not a law, and it is the reason a count-mode overview of a clip whose important moment is in the opening second will show you nothing.
The other property worth knowing: the spacing is uniform in time, not in content. A five-frame overview of a lecture is five evenly spaced frames, four of which may be the same slide. Count mode cannot know that, and no amount of tuning N fixes it. If the clip has chapters, they are a better source of sample points than arithmetic.
Interval mode: the trailing segment disappears
Interval mode requests 0, s, 2s and so on while the running value is strictly less than the duration. Two consequences follow directly from that loop condition.
- The final partial segment is never sampled. A 10-second clip at 3-second intervals yields 0, 3, 6 and 9 — the last second is covered by nothing.
- The counter accumulates in floating point. After a few hundred steps, adding 0.1 repeatedly does not land on exactly 12.3, and the requested times drift by tiny amounts that are irrelevant for review and worth knowing about if you are reconciling a list of timestamps against a log.
The practical effect of the first point is that interval mode is biased towards the beginning of a clip. If you are sampling a two-hour recording at 60-second intervals to find where something changed, the change is just as likely to be at 1:59:30 as at 0:00:30, but your last sample is at 1:59:00 and the final minute is invisible. Add one timestamp at the end, or use count mode, which distributes the same number of samples across the whole clip.
Keyframe mode is a two-second grid, and it says so here
This is the part of the interface that needs the most honest description, because the label is a promise the implementation cannot keep.
Keyframe mode in this tool steps through the clip at a fixed two-second interval. It does not read the container’s keyframe index. It does not know where the real keyframes are. It is a sampling grid with a name that suggests otherwise.
So what is the two-second grid actually for? A first pass over long footage where you want roughly one candidate per conversational beat, and where the alternative — scrubbing a two-hour timeline by hand — is much worse. It is a convenience, not an index. If you need the real keyframe positions, they are one command away:
ffprobe -v error -select_streams v:0 \
-show_entries frame=key_frame,pts_time \
-of csv=p=0 input.mp4 | awk -F, '$1 == 1 { print $2 }'One trap in that command is worth a sentence, because it cost us a wrong answer. ffprobe prints fields in its own internal order, not in the order you list them: asking for pts_time,key_frame still yields key_frame first. An awk written against the order you asked for silently matches nothing and prints an empty list, which looks exactly like a file with no keyframes. Check the first line of output before trusting the filter.
The output is the ground truth the browser tool is approximating. On a typical export with a 2-second GOP the two lists will look similar. On a file with scene-cut keyframes — most professionally edited footage — they will not: the real keyframes land on cuts, which is far more useful for reviewing an edit than an arbitrary grid, and it is the reason a desktop tool that reads the index can find cut points in seconds.
What actually happens when you ask for 12.500 seconds
The request is a seek, and a seek is not a lookup. Compressed video stores most frames as differences from other frames. To display the frame at 12.500 s the decoder must start at the nearest earlier keyframe and decode forward until it reaches the requested time. Everything between the keyframe and the target is decoded and discarded, which is why seeking in a long-GOP file is slower than seeking in a dense one, and why the cost does not scale with how far you jump but with how far you are from the previous keyframe.
Three things follow that matter for a still image.
- The frame you get covers an interval, not an instant. At 30 fps each frame is visible for about 33 ms. Asking for 12.500 s gets you the frame whose presentation interval contains 12.500 s; asking for 12.505 s gets the same frame, and asking for 12.517 s gets the next one. The still cannot be more precise than the frame rate allows.
- Browsers may adjust the reported time. After a seek,
currentTimecan be set to the actual position the decoder reached, which may differ from what you requested by a frame or so. ReadingcurrentTimeback after theseekedevent is the only way to know what the browser thinks it is showing. - Variable frame rate files make the arithmetic non-obvious. In a screen recording that drops to 5 fps during a static stretch, one second of media time may contain five frames instead of thirty, and the relationship between a timestamp and a frame index is not linear.
None of this makes the exported still wrong. It makes it approximate at the level of one frame interval, which is fine for a thumbnail and not fine for a claim like “at 00:01:14.500 the dialog was already open”. For the second kind of claim, the next section gives you a way to check.
Measure the drift on your own file
The only way to know how far off a seek lands is to make the frame number visible in the picture, extract from that copy, and read the number back. The obvious way to do this is to burn the digits in with drawtext and read them by eye. The better way is to burn a pattern a canvas can read, because then the check can run hundreds of times instead of four, and nobody has to squint at a screenshot.
# 20 s at 30 fps = 600 frames. Bar A encodes n mod 30, bar B encodes floor(n / 30).
# drawbox cannot do this: its x expression is evaluated once, so every frame is identical.
# geq re-evaluates per frame and has N, hence geq. Commas inside it must be escaped.
XA='(N-30*floor(N/30))*10.6'
ffmpeg -f lavfi -i "color=c=black:s=320x180:r=30:duration=20" \
-vf "geq=lum='235*max(gt(Y\,19)*lt(Y\,45)*gt(X\,${XA}-1)*lt(X\,${XA}+7),gt(Y\,59)*lt(Y\,85)*gt(X\,floor(N/30)*16-1)*lt(X\,floor(N/30)*16+11))+16':cb=128:cr=128" \
-c:v libx264 -preset veryfast -crf 20 -pix_fmt yuv420p \
-g 250 -keyint_min 250 -sc_threshold 0 \
counter.mp4Two bars, 30 positions and 20 positions, which is 600 unambiguous frames. A canvas finds each bar’s centre column and multiplies back out; a person looking at the still sees which column is lit. Because the pattern is high-contrast and coarse, H.264 at a normal quality setting does not blur the bars into each other — the index survives the very compression the measurement is about.
We ran this against a 20-second, 30-frame-per-second clip (600 frames, keyframes every 250 frames, so at 0, 8.333 and 16.667 seconds) in Google Chrome for Testing 145.0.7632.6, headless, on macOS on Apple silicon. The file was served from localhost over HTTP with byte-range support, so seeking behaved like a remote file rather than a blob in memory. Each mode requested the times the tool would request, waited for seeked, drew the frame, and read the index back.
| Mode | Seeks | Landed on the expected frame | Difference (frames) | Median seek |
|---|---|---|---|---|
| Count, N = 10 | 10 | 10 | 0 in every sample | 4 ms |
| Interval, 2 s | 10 | 10 | 0 in every sample | 4 ms |
| Keyframes, 2 s grid | 10 | 10 | 0 in every sample | 4 ms |
| Timestamps, 4 values | 4 | 4 | 0 in every sample | 4 ms |
| Requested (s) | Reported (s) | Expected frame | Frame delivered | Difference | Seek |
|---|---|---|---|---|---|
| 1.234 | 1.234 | 37 | 37 | 0 | 2 ms |
| 5.455 | 5.455 | 163 | 163 | 0 | 5 ms |
| 12.345 | 12.345 | 370 | 370 | 0 | 4 ms |
| 16.364 | 16.364 | 490 | 490 | 0 | 7 ms |
| 18.900 | 18.900 | 567 | 567 | 0 | 3 ms |
Zero difference in all 34 samples is the result, and it is worth being clear about what it means. Chrome does not snap to the nearest keyframe. It decodes forward from the previous one and presents the frame whose presentation interval contains the requested instant — at 18.182 s that meant frame 545, which is 45 frames past the keyframe at 500. The expensive part is real: the decoder did that work. It just did not lose accuracy doing it.
The practical consequence is a shift in where to look for error. If the seek is exact, a still that lands in the wrong place is almost always the arithmetic: a count mode asked for N points when the event sits between two of them, an interval that never sampled the final minute, or a timestamp computed from a 30 fps assumption applied to 29.97 fps footage — which accumulates about one frame of error per 33 seconds, and a full second by the end of a 33-minute recording.
What the output format does to the pixels
Extraction accuracy has a second half that gets less attention: the frame was decoded correctly, and then it was written to a file. PNG is lossless and large; JPEG is lossy and small; the interesting question is how much smaller and how much lossy, on the two kinds of image people actually extract.
We encoded two synthetic frames with the browser’s own canvas.toBlob — one text-heavy frame with burned-in digits, one continuous-tone image with fine detail — and measured both the file size and the peak signal-to-noise ratio against the PNG original, inside the same page that produced the stills.
| Image | PNG | JPEG q 0.92 | JPEG q 0.80 |
|---|---|---|---|
| Text-heavy, 640×360 | 82,178 B | 25,223 B (31% of PNG), PSNR 29.38 dB | 17,625 B (21%), PSNR 28.92 dB |
| Continuous tone, 1280×720 | 789,559 B | 102,709 B (13% of PNG), PSNR 27.90 dB | 63,891 B (8%), PSNR 27.62 dB |
Read the ratios, not the decibels. On the photographic image JPEG is roughly an eighth of the PNG at the higher quality setting, and the loss is spread thinly across texture where nobody will find it. On the text image JPEG is a third of the PNG, and the loss is concentrated in the one place it is most visible: the edges of glyphs, which is exactly what makes a burned-in timestamp legible or illegible.
Choose by the cost of being wrong
There is no best mode. There is a mode whose failure you can live with. Sort your task by what happens if the still lands a second away from the interesting moment.
| If being one second off… | Use | Why not the alternative |
|---|---|---|
| is invisible (a thumbnail, a mood board) | Count, 8 to 16 | Interval would produce more frames than you will review, and reviewing is the slow part. |
| means you redo the pass (a first look at unfamiliar footage) | Keyframes, then timestamps | The two-second grid is cheap; the refinement pass is where the accuracy comes from. |
| means the reader misunderstands a step (a tutorial) | Timestamps from the transcript | A grid cannot know where a step begins. Only the transcript or your own notes do. |
| means the evidence is wrong (QA, incident, audit) | Timestamps from a log, verified once against a counter | Anything else is a sample that happens to be near the event. |
| means a legal or safety claim fails | Not this tool | Use a deterministic engine that reports exact frame numbers and keeps a log, and preserve the source file. |
The last row is not modesty. A browser seek is a best-effort operation on a stream it did not index, and no amount of interface polish changes that. The guide on evidence takes this further.
Questions this guide gets asked
Why does count mode skip the first and last frame?
It divides the duration by N + 1 and samples the interior points, so a 60-second clip with N = 5 gives 10, 20, 30, 40 and 50 seconds. The first and last frames of an export are often fades, title cards or trim boundaries, so including them tends to make an overview start with a black rectangle. If you need the opening frame, request 0 in timestamp mode.
Is keyframe mode reading the file’s real keyframes?
No. It steps on a fixed two-second grid. The <video> element has no API that exposes the sample table, so a browser-only tool cannot read the keyframe index without demuxing the file itself. Use ffprobe -show_entries frame=key_frame,pts_time if you need the real positions — and note that ffprobe prints those fields in that order whatever order you list them in.
Why is the reported timestamp exactly what I asked for, even when the frame is not?
Because it is a label for the request, not a measurement of the result. The exported still is whatever the decoder presented at that moment, which may be up to one frame interval earlier. Reading video.currentTime back after the seeked event, or burning a counter into a test copy, is how you check the difference.
Does a shorter interval always give better coverage?
It gives more frames, which is not the same thing. On a mostly static clip, a one-second interval produces hundreds of near-duplicates and the review cost rises faster than the information does. Coverage in time is cheap; coverage in content depends on where the content changes, which is what chapter markers and transcripts are for.
Can I get an exact frame number instead of a timestamp?
Not from a browser video element, which deals in time rather than frame indices. A deterministic engine such as FFmpeg can select frame N directly and print the exact timestamps it produced, which is the right tool when the number itself is the evidence.