Skip to content

feat: read approximate frames - #11

Merged
alesanfra merged 2 commits into
mainfrom
feat/approximate-frames
Sep 20, 2026
Merged

alesanfra merged 2 commits into
mainfrom
feat/approximate-frames

Conversation

@alesanfra

Copy link
Copy Markdown
Owner

Reading a frame in the middle of a group of pictures costs every frame from the key frame on. When a frame nearby will do, approximate reads the key frame nearest to each number in frames instead, which costs one decoded frame.

# The nearest key frame within 5 frames, else the frame itself.
frames = list(iterframes.read("video.mp4", frames=[100, 200, 300], approximate=5))

The tolerance is the point. approximate=True accepts any distance, which in a video with a key frame every ten seconds answers three requests a second apart with the same frame; a number bounds the error instead, and a frame with no key frame that close is still read exactly. False, 0 and None read every frame exactly, and approximate with a slice raises ValueError.

What changed

  • Selection::Frames carries the tolerance, so the type says the option goes with frames rather than a slice. selected maps each index through the new Seeker::nearest_key before decoding, and Seeker::frame is untouched.
  • The index pass still reads every packet: the saving is in decoding alone. Two numbers that snap to the same key frame each get that frame, so the caller receives one frame per number asked for, in the order asked for.
  • read, read_batches and FrameReader take approximate; the docs, the README and the reference describe it.

Tests

Nine cases in tests/test_select.py cover snapping to the nearest key frame, a tolerance that leaves distant frames exact, key frames that stand for themselves, repeats, the off switch, batches and both errors. test_approximate_frames_decode_less in tests/test_benchmark.py measures the saving: 0.018s exact against 0.010s approximate on the test video.

The landing page

The random access demo gained an Exact / approximate=45 switch that redraws the whole figure, so the difference is visible rather than described, and the heading now names what the figure shows.

The page is also restyled: warm paper, cards with a dashed inner edge, buttons that press, code in recessed slabs, sprocket holes on the strip of frames, and two self-hosted typefaces (Plus Jakarta Sans and JetBrains Mono, ~30 KB each, SIL OFL). No build step and no third-party request, as before. The social card is now rendered from web/og-card.html against the same stylesheet, so it cannot drift from the site.

🤖 Generated with Claude Code

alesanfra and others added 2 commits September 20, 2026 16:17
With a list of frame numbers, `approximate` reads each one as the key
frame nearest to it, when that key frame is no further away than the
number of frames given. Decoding a frame in the middle of a group of
pictures costs every frame from the key frame on; a key frame costs one.

The tolerance is what makes it usable: `approximate=5` moves a frame by
at most five frames and reads the rest exactly, so the error stays inside
a number the caller chose, while `approximate=True` accepts any distance.
A video with a key frame every ten seconds would otherwise answer three
requests a second apart with the same frame.

`Selection::Frames` carries the tolerance, so the type says the option
goes with `frames` and not with a slice, and `selected` maps each index
through `Seeker::nearest_key` before decoding. Nothing else changes: the
index pass still reads every packet, and two numbers that snap to the
same key frame each get that frame, so the caller still receives one
frame per number asked for, in the order asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The random access demo now runs in two modes. `Exact` decodes from the
key frame in front of each number, `approximate=45` reads the nearest key
frame within 45 frames, and the buttons above the figure switch between
them: the counters, the steps, the zoomed window, the frames handed back
and the call under the figure all come from the plan of the mode showing,
so the difference is the drawing rather than a sentence about it. Both
plans are built by `seekPlan` from the constants at the top of `main.js`,
as everything else on the page is. The heading said "pay for twelve
frames", a number that appears nowhere in the demo, and now says what the
figure shows.

The page itself was a grid of hairlines on flat grey. It is now warm
paper with objects on it: cards carry a dashed inner edge, buttons press
down, code sits in a recessed slab, and the strip of frames has sprocket
holes. Two shadows do all the depth, `--lift` for what sits on the page
and `--well` for what is cut into it. Each accent is one `--x-rgb` triple
that every wash in every diagram reuses, so amber still means frames,
cyan the decoder thread and magenta the GPU.

Two typefaces replace the system stack: Plus Jakarta Sans for text and
headlines, JetBrains Mono for code, numbers and labels. Both are
self-hosted latin-only woff2 files of about 30 KB under the SIL Open Font
License, so the page still makes no third-party request and still has no
build step.

The social card is no longer a separate composition that drifts from the
site: `og-card.html` lays it out against `styles.css`, and `README.md`
has the command that renders it. `AGENTS.md` now says to keep the landing
page in step with the code it documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alesanfra
alesanfra merged commit 2158899 into main Sep 20, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant