iterframes 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iterframes-0.6.0/.github/scripts/check_site.py +50 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/.github/workflows/ci.yaml +79 -1
- iterframes-0.6.0/.github/workflows/pages.yaml +61 -0
- iterframes-0.6.0/.release-please-manifest.json +3 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/AGENTS.md +69 -9
- {iterframes-0.4.0 → iterframes-0.6.0}/CHANGELOG.md +20 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/Cargo.lock +1 -1
- {iterframes-0.4.0 → iterframes-0.6.0}/Cargo.toml +1 -1
- iterframes-0.6.0/PKG-INFO +209 -0
- iterframes-0.6.0/README.md +185 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/build.rs +17 -1
- {iterframes-0.4.0 → iterframes-0.6.0}/docs/development.md +34 -4
- {iterframes-0.4.0 → iterframes-0.6.0}/docs/index.md +23 -1
- {iterframes-0.4.0 → iterframes-0.6.0}/docs/reference.md +107 -13
- iterframes-0.6.0/iterframes/__init__.py +163 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/pyproject.toml +4 -1
- {iterframes-0.4.0 → iterframes-0.6.0}/scripts/build-ffmpeg.sh +46 -6
- iterframes-0.6.0/src/decoder.rs +671 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/src/ffmpeg.rs +202 -25
- {iterframes-0.4.0 → iterframes-0.6.0}/src/lib.rs +196 -42
- iterframes-0.6.0/tests/conftest.py +57 -0
- iterframes-0.6.0/tests/test_benchmark.py +136 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/tests/test_read.py +107 -15
- iterframes-0.6.0/tests/test_select.py +394 -0
- iterframes-0.6.0/web/README.md +123 -0
- iterframes-0.6.0/web/assets/favicon.svg +5 -0
- iterframes-0.6.0/web/assets/fonts/jakarta-latin.woff2 +0 -0
- iterframes-0.6.0/web/assets/fonts/jetbrains-mono-latin.woff2 +0 -0
- iterframes-0.6.0/web/assets/frames.webp +0 -0
- iterframes-0.6.0/web/assets/og.png +0 -0
- iterframes-0.6.0/web/assets/scenes.webp +0 -0
- iterframes-0.6.0/web/index.html +532 -0
- iterframes-0.6.0/web/main.js +467 -0
- iterframes-0.6.0/web/og-card.html +51 -0
- iterframes-0.6.0/web/styles.css +730 -0
- iterframes-0.4.0/.release-please-manifest.json +0 -3
- iterframes-0.4.0/PKG-INFO +0 -106
- iterframes-0.4.0/README.md +0 -82
- iterframes-0.4.0/iterframes/__init__.py +0 -88
- iterframes-0.4.0/src/decoder.rs +0 -183
- iterframes-0.4.0/tests/conftest.py +0 -39
- iterframes-0.4.0/tests/test_benchmark.py +0 -61
- {iterframes-0.4.0 → iterframes-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/.gitignore +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/.pre-commit-config.yaml +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/.readthedocs.yaml +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/CLAUDE.md +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/LICENSE +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/mkdocs.yml +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/release-please-config.json +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/src/dlpack.rs +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/tests/data/video_480x270.mp4 +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/tests/data/video_av1_480x270.mp4 +0 -0
- {iterframes-0.4.0 → iterframes-0.6.0}/uv.lock +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Check that every local reference in the landing page resolves to a file.
|
|
2
|
+
|
|
3
|
+
The page has no build step, so nothing else would catch a renamed asset or a
|
|
4
|
+
typo in a path until the deployed site showed a broken image.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import unquote, urlparse
|
|
11
|
+
|
|
12
|
+
# href="...", src="..." and url(...) in CSS.
|
|
13
|
+
REFERENCES = re.compile(
|
|
14
|
+
r"""(?:href|src)\s*=\s*["']([^"']+)["']|url\(\s*["']?([^"')]+)["']?\s*\)"""
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def local(reference: str) -> bool:
|
|
19
|
+
parsed = urlparse(reference)
|
|
20
|
+
return not parsed.scheme and not parsed.netloc and bool(parsed.path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(root: Path) -> int:
|
|
24
|
+
missing = []
|
|
25
|
+
for source in sorted(root.rglob("*")):
|
|
26
|
+
if source.suffix not in {".html", ".css", ".js"}:
|
|
27
|
+
continue
|
|
28
|
+
text = source.read_text(encoding="utf-8")
|
|
29
|
+
for match in REFERENCES.finditer(text):
|
|
30
|
+
reference = match.group(1) or match.group(2)
|
|
31
|
+
if not local(reference):
|
|
32
|
+
continue
|
|
33
|
+
path = unquote(urlparse(reference).path)
|
|
34
|
+
if path in {"./", "."}:
|
|
35
|
+
continue
|
|
36
|
+
target = (source.parent / path).resolve()
|
|
37
|
+
if not target.exists():
|
|
38
|
+
line = text.count("\n", 0, match.start()) + 1
|
|
39
|
+
missing.append(f"{source}:{line}: {reference}")
|
|
40
|
+
|
|
41
|
+
for problem in missing:
|
|
42
|
+
print(f"missing: {problem}", file=sys.stderr)
|
|
43
|
+
if missing:
|
|
44
|
+
return 1
|
|
45
|
+
print(f"every local reference under {root} resolves")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else "web")))
|
|
@@ -8,11 +8,20 @@ name: CI
|
|
|
8
8
|
# updates its release pull request, and merging that pull request creates
|
|
9
9
|
# the tag and a draft GitHub release, then the jobs below attach the
|
|
10
10
|
# wheels, publish them to PyPI, and publish the release.
|
|
11
|
+
# The landing page has its own workflow, and nothing here builds it, so a
|
|
12
|
+
# commit that only touches web/ skips the wheel matrix. release-please skips
|
|
13
|
+
# with it, and picks those commits up on the next push that does build
|
|
14
|
+
# something.
|
|
11
15
|
on:
|
|
12
16
|
push:
|
|
13
17
|
branches:
|
|
14
18
|
- main
|
|
19
|
+
paths-ignore: &page
|
|
20
|
+
- 'web/**'
|
|
21
|
+
- '.github/workflows/pages.yaml'
|
|
22
|
+
- '.github/scripts/check_site.py'
|
|
15
23
|
pull_request:
|
|
24
|
+
paths-ignore: *page
|
|
16
25
|
workflow_dispatch:
|
|
17
26
|
|
|
18
27
|
permissions:
|
|
@@ -147,6 +156,75 @@ jobs:
|
|
|
147
156
|
uv pip install --reinstall-package iterframes dist/*.whl
|
|
148
157
|
uv run --no-sync pytest
|
|
149
158
|
|
|
159
|
+
windows:
|
|
160
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
161
|
+
strategy:
|
|
162
|
+
matrix:
|
|
163
|
+
platform:
|
|
164
|
+
- runner: windows-2025
|
|
165
|
+
target: x64
|
|
166
|
+
defaults:
|
|
167
|
+
run:
|
|
168
|
+
# MSYS2 runs the FFmpeg build script; path-type inherit keeps MSVC,
|
|
169
|
+
# cargo, and uv on PATH, after MSYS2's own tools.
|
|
170
|
+
shell: msys2 {0}
|
|
171
|
+
steps:
|
|
172
|
+
- uses: actions/checkout@v7
|
|
173
|
+
- name: Set up MSVC
|
|
174
|
+
uses: ilammy/msvc-dev-cmd@v1
|
|
175
|
+
with:
|
|
176
|
+
arch: ${{ matrix.platform.target }}
|
|
177
|
+
- name: Set up MSYS2
|
|
178
|
+
uses: msys2/setup-msys2@v2
|
|
179
|
+
with:
|
|
180
|
+
msystem: UCRT64
|
|
181
|
+
path-type: inherit
|
|
182
|
+
# Native pkg-config and nasm; FFmpeg and dav1d compile with MSVC.
|
|
183
|
+
install: >-
|
|
184
|
+
make diffutils curl tar xz
|
|
185
|
+
mingw-w64-ucrt-x86_64-pkgconf
|
|
186
|
+
mingw-w64-ucrt-x86_64-nasm
|
|
187
|
+
- name: Remove MSYS2's link
|
|
188
|
+
# It would shadow MSVC's link.exe, for rustc among others.
|
|
189
|
+
run: rm -f /usr/bin/link.exe
|
|
190
|
+
- name: Install uv
|
|
191
|
+
uses: astral-sh/setup-uv@v10.1.0
|
|
192
|
+
- name: Install meson and ninja
|
|
193
|
+
# With a native Python, so that meson sees Windows and uses MSVC.
|
|
194
|
+
run: |
|
|
195
|
+
uv tool install meson
|
|
196
|
+
uv tool install ninja
|
|
197
|
+
echo "$(cygpath -u "$(uv tool dir --bin)")" >> "$GITHUB_PATH"
|
|
198
|
+
- name: Cache FFmpeg
|
|
199
|
+
uses: actions/cache@v6
|
|
200
|
+
with:
|
|
201
|
+
path: build/ffmpeg
|
|
202
|
+
key: ffmpeg-windows-${{ matrix.platform.target }}-${{ hashFiles('scripts/build-ffmpeg.sh') }}
|
|
203
|
+
- name: Build wheels
|
|
204
|
+
run: |
|
|
205
|
+
uv sync --frozen --no-install-project
|
|
206
|
+
uv run --no-sync maturin build --release --out dist
|
|
207
|
+
- name: Show the FFmpeg build log
|
|
208
|
+
if: failure()
|
|
209
|
+
run: |
|
|
210
|
+
tail -n 200 build/ffmpeg.log
|
|
211
|
+
cat build/ffmpeg/lib/pkgconfig/*.pc
|
|
212
|
+
- name: Upload wheels
|
|
213
|
+
uses: actions/upload-artifact@v7
|
|
214
|
+
with:
|
|
215
|
+
name: wheels-windows-${{ matrix.platform.target }}
|
|
216
|
+
path: dist
|
|
217
|
+
- name: Run tests
|
|
218
|
+
run: |
|
|
219
|
+
for python in 3.11 3.14; do
|
|
220
|
+
uv sync --frozen --no-install-project --python $python
|
|
221
|
+
uv pip install --reinstall-package iterframes dist/*.whl
|
|
222
|
+
uv run --no-sync pytest
|
|
223
|
+
done
|
|
224
|
+
- name: Lint Rust
|
|
225
|
+
# The only job that compiles the Windows code paths.
|
|
226
|
+
run: cargo clippy --all-targets -- -D warnings
|
|
227
|
+
|
|
150
228
|
sdist:
|
|
151
229
|
runs-on: ubuntu-latest
|
|
152
230
|
steps:
|
|
@@ -166,7 +244,7 @@ jobs:
|
|
|
166
244
|
name: Attach to the GitHub release
|
|
167
245
|
runs-on: ubuntu-latest
|
|
168
246
|
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
|
169
|
-
needs: [release-please, lint, linux, macos, sdist]
|
|
247
|
+
needs: [release-please, lint, linux, macos, windows, sdist]
|
|
170
248
|
permissions:
|
|
171
249
|
# Used to sign the attestations
|
|
172
250
|
id-token: write
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Publishes web/ to https://alesanfra.github.io/iterframes/.
|
|
2
|
+
#
|
|
3
|
+
# The page is plain HTML, CSS and JavaScript, so there is nothing to build:
|
|
4
|
+
# the job uploads the directory and hands it to Pages. It runs only when
|
|
5
|
+
# web/ changes, and ci.yaml ignores web/ for the same reason, so a change
|
|
6
|
+
# to one never spends minutes on the other.
|
|
7
|
+
#
|
|
8
|
+
# Pages must be set to "GitHub Actions" under Settings > Pages for the
|
|
9
|
+
# deployment to be accepted.
|
|
10
|
+
name: Pages
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches:
|
|
15
|
+
- main
|
|
16
|
+
paths: &page
|
|
17
|
+
- 'web/**'
|
|
18
|
+
- '.github/workflows/pages.yaml'
|
|
19
|
+
- '.github/scripts/check_site.py'
|
|
20
|
+
pull_request:
|
|
21
|
+
paths: *page
|
|
22
|
+
workflow_dispatch:
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
|
|
27
|
+
# One deployment at a time, and never cancel one halfway: a cancelled
|
|
28
|
+
# deployment leaves the live site on the previous version.
|
|
29
|
+
concurrency:
|
|
30
|
+
group: pages
|
|
31
|
+
cancel-in-progress: false
|
|
32
|
+
|
|
33
|
+
jobs:
|
|
34
|
+
check:
|
|
35
|
+
name: Check the page
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v7
|
|
39
|
+
- name: Check every local link and asset resolves
|
|
40
|
+
run: python3 .github/scripts/check_site.py web
|
|
41
|
+
|
|
42
|
+
deploy:
|
|
43
|
+
name: Deploy to GitHub Pages
|
|
44
|
+
# Pull requests get the check above; only main is published.
|
|
45
|
+
if: ${{ github.event_name != 'pull_request' }}
|
|
46
|
+
needs: check
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
permissions:
|
|
49
|
+
pages: write
|
|
50
|
+
id-token: write
|
|
51
|
+
environment:
|
|
52
|
+
name: github-pages
|
|
53
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v7
|
|
56
|
+
- uses: actions/configure-pages@v6
|
|
57
|
+
- uses: actions/upload-pages-artifact@v5
|
|
58
|
+
with:
|
|
59
|
+
path: web
|
|
60
|
+
- id: deployment
|
|
61
|
+
uses: actions/deploy-pages@v5
|
|
@@ -17,7 +17,7 @@ decodes the next ones. Keep that true, and say so in the docs.
|
|
|
17
17
|
|
|
18
18
|
The package uses maturin's mixed layout: the compiled module is installed
|
|
19
19
|
as `iterframes.iterframes`, and `iterframes/__init__.py` wraps its
|
|
20
|
-
`FrameReader` into `read` and `
|
|
20
|
+
`FrameReader` into `read` and `read_batches`.
|
|
21
21
|
|
|
22
22
|
## How decoding works
|
|
23
23
|
|
|
@@ -36,6 +36,31 @@ as `iterframes.iterframes`, and `iterframes/__init__.py` wraps its
|
|
|
36
36
|
Python code holds it. Never attach to Python there (no `Python::attach`,
|
|
37
37
|
no Python objects in `decoder.rs`); `test_benchmark.py` checks the
|
|
38
38
|
overlap.
|
|
39
|
+
- With `batch_size`, the thread allocates one `ffmpeg::Buffer` per batch
|
|
40
|
+
and swscale writes each frame into its slice (`Scaler::run_into`); the
|
|
41
|
+
batch is sent when full, or at the end of the video unless `drop_last`.
|
|
42
|
+
Python gets it as a `Batch`, a 4-dimensional buffer, with no copy. The
|
|
43
|
+
channel then holds batches: `prefetch_frames` rounds up to whole ones.
|
|
44
|
+
- `frames`, or `start`, `stop`, and `step`, pick the frames to decode by
|
|
45
|
+
number. `start=0` with `step=1` is `Selection::First`, which reads the
|
|
46
|
+
video straight through and stops early, with no index and no seek: keep
|
|
47
|
+
that path, since it is the common one. Otherwise the thread indexes the
|
|
48
|
+
file first (`index` in
|
|
49
|
+
`src/decoder.rs`), demuxing every packet without decoding it to map each
|
|
50
|
+
frame to its timestamp and mark the key frames, and `Seeker` decodes each
|
|
51
|
+
frame asked for from the key frame before it, or goes on from the frame
|
|
52
|
+
it decoded last when that is closer, or when the stream does not seek. Frames passed on the way are never
|
|
53
|
+
converted to RGB. The index leaves out the frames before the first key
|
|
54
|
+
frame, which decoding cannot return either, so frame numbers are those of
|
|
55
|
+
a plain `read`. A stream that cannot seek, such as raw H.264, is opened
|
|
56
|
+
again and read from the start (`Source::open`).
|
|
57
|
+
- `approximate` goes with `frames` only, and carries a tolerance in frames
|
|
58
|
+
(`True` is `usize::MAX`), in `Selection::Frames`. `selected` then maps
|
|
59
|
+
each index through `Seeker::nearest_key`, which snaps it to the key frame
|
|
60
|
+
closest to it when that is within the tolerance, and leaves it alone
|
|
61
|
+
otherwise. Nothing else changes: the index pass still runs, and two
|
|
62
|
+
indices that snap to the same key frame decode it once each, so the caller
|
|
63
|
+
gets one frame per number asked for.
|
|
39
64
|
- Errors travel through the channel and become Python exceptions in
|
|
40
65
|
`impl From<Error> for PyErr`. A closed channel means the end of the video.
|
|
41
66
|
- Dropping the reader closes the channel; the thread notices on its next
|
|
@@ -54,32 +79,38 @@ as `iterframes.iterframes`, and `iterframes/__init__.py` wraps its
|
|
|
54
79
|
as `AVERROR` are written by hand in the `sys` module of `src/ffmpeg.rs`.
|
|
55
80
|
- The build disables autodetection, so the wheel depends on libc and
|
|
56
81
|
system frameworks only; dav1d is added for AV1.
|
|
82
|
+
- On Windows the script runs in MSYS2 but compiles with MSVC
|
|
83
|
+
(`--toolchain=msvc`, `-MD`), because the wheels target
|
|
84
|
+
`x86_64-pc-windows-msvc`; it copies each `libx.a` to `x.lib` for MSVC's
|
|
85
|
+
linker. `build.rs` finds `bash.exe` on `PATH` itself, since Rust would
|
|
86
|
+
otherwise pick WSL's from the system directory.
|
|
57
87
|
- Keep the build LGPL: never pass `--enable-gpl` or `--enable-nonfree`.
|
|
58
88
|
- Hardware decoding (`device="mps"` / `"cuda"`, PyTorch's names, mapped
|
|
59
89
|
to FFmpeg's in `hardware_devices` in `src/lib.rs`): VideoToolbox on
|
|
60
|
-
macOS; on Linux
|
|
61
|
-
decoders, which load the NVIDIA driver
|
|
62
|
-
GPU. Both add no library to the wheel. The macOS build needs clang's
|
|
90
|
+
macOS; on Linux and Windows
|
|
91
|
+
the `*_cuvid` decoders, which load the NVIDIA driver at run time and
|
|
92
|
+
resize on the GPU. Both add no library to the wheel. The macOS build needs clang's
|
|
63
93
|
compiler-rt for `@available`, which `build.rs` links.
|
|
64
94
|
- NVDEC has never run on a GPU in this project: CI has none, and the
|
|
65
95
|
`cuda` tests skip when the device does not open. The same goes for
|
|
66
96
|
`on_device=True` (`CudaFrame`, `Plane`, `src/dlpack.rs`), which waits on
|
|
67
|
-
cuvid's copy with CUDA driver calls found through dlopen
|
|
68
|
-
(`ffmpeg::cuda`).
|
|
97
|
+
cuvid's copy with CUDA driver calls found through dlopen, or
|
|
98
|
+
`LoadLibraryA` on Windows (`ffmpeg::cuda`).
|
|
69
99
|
|
|
70
100
|
## Layout
|
|
71
101
|
|
|
72
102
|
| Path | Contents |
|
|
73
103
|
| --- | --- |
|
|
74
|
-
| `src/lib.rs` | PyO3 module: `Frame`, `FrameReader`, error mapping, module init |
|
|
104
|
+
| `src/lib.rs` | PyO3 module: `Frame`, `Batch`, `FrameReader`, error mapping, module init |
|
|
75
105
|
| `src/decoder.rs` | Decoding thread |
|
|
76
106
|
| `src/ffmpeg.rs` | Safe wrappers over the FFmpeg calls the crate needs |
|
|
77
107
|
| `src/dlpack.rs` | DLPack capsules for the planes of `CudaFrame` |
|
|
78
|
-
| `iterframes/__init__.py` | `read`, `
|
|
108
|
+
| `iterframes/__init__.py` | `read`, `read_batches` |
|
|
79
109
|
| `build.rs` | Builds and links FFmpeg, generates its bindings |
|
|
80
110
|
| `scripts/build-ffmpeg.sh` | Static FFmpeg and dav1d, run by `build.rs` |
|
|
81
111
|
| `tests/` | pytest suite; frames are compared with PyAV |
|
|
82
112
|
| `docs/` | MkDocs site published on Read the Docs |
|
|
113
|
+
| `web/` | Landing page published on GitHub Pages |
|
|
83
114
|
|
|
84
115
|
## Environment
|
|
85
116
|
|
|
@@ -110,7 +141,8 @@ uv run --no-sync pytest
|
|
|
110
141
|
```
|
|
111
142
|
|
|
112
143
|
CI runs the same four, plus `ruff check .` and `ruff format --check .` for
|
|
113
|
-
the Python files.
|
|
144
|
+
the Python files. Only the `windows` job compiles the `cfg(windows)`
|
|
145
|
+
code, and runs clippy on it. `pre-commit run -a` covers the formatters and linters
|
|
114
146
|
locally.
|
|
115
147
|
|
|
116
148
|
## Docs
|
|
@@ -121,6 +153,34 @@ built module; check them instead of writing them from memory. Keep the
|
|
|
121
153
|
text short, in American English, with no performance claims that have not
|
|
122
154
|
been measured.
|
|
123
155
|
|
|
156
|
+
The landing page in `web/` is part of the docs: whenever an argument, a
|
|
157
|
+
default, or the behavior of a feature changes, update it in the same
|
|
158
|
+
change as the code, and say so in the pull request. Its diagrams are
|
|
159
|
+
drawn from the numbers declared in `web/main.js`, so change the number
|
|
160
|
+
and let the drawing and the captions follow; never write a figure into
|
|
161
|
+
the HTML that the script also computes. `web/README.md` lists those
|
|
162
|
+
numbers and the files. Check the result in a browser before proposing the
|
|
163
|
+
change: open `web/index.html`, or serve the directory with
|
|
164
|
+
`python3 -m http.server --directory web`. The page has no build step and no
|
|
165
|
+
third-party request: its two typefaces are self-hosted woff2 files in
|
|
166
|
+
`web/assets/fonts/`, and everything else is plain HTML, CSS, and JavaScript.
|
|
167
|
+
`web/README.md` describes the look and the colour rules; keep them rather
|
|
168
|
+
than restyling one component on its own.
|
|
169
|
+
|
|
170
|
+
## Landing page
|
|
171
|
+
|
|
172
|
+
`web/` is the page at
|
|
173
|
+
[alesanfra.github.io/iterframes](https://alesanfra.github.io/iterframes/):
|
|
174
|
+
plain HTML, CSS and JavaScript, no build step, no framework, no web fonts and
|
|
175
|
+
no third-party script. `.github/workflows/pages.yaml` uploads the directory
|
|
176
|
+
and runs only when it changes; `ci.yaml` ignores it, so neither spends minutes
|
|
177
|
+
on the other. Its two animated diagrams are built from constants at the top of
|
|
178
|
+
`main.js`, and their captions are computed from the same constants, so a
|
|
179
|
+
number cannot appear in the drawing and the prose with two different values.
|
|
180
|
+
`web/README.md` has the `ffmpeg` commands that made the images.
|
|
181
|
+
|
|
182
|
+
Keep its claims to what the benchmarks measured, as in the docs.
|
|
183
|
+
|
|
124
184
|
## Conventions
|
|
125
185
|
|
|
126
186
|
- Commits follow [Conventional Commits](https://www.conventionalcommits.org/),
|
|
@@ -6,6 +6,26 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
6
6
|
From 0.4.0 on, [release-please](https://github.com/googleapis/release-please)
|
|
7
7
|
writes each entry from the Conventional Commits since the previous release.
|
|
8
8
|
|
|
9
|
+
## [0.6.0](https://github.com/alesanfra/iterframes/compare/v0.5.0...v0.6.0) (2026-09-20)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* read approximate frames ([#11](https://github.com/alesanfra/iterframes/issues/11)) ([2158899](https://github.com/alesanfra/iterframes/commit/2158899f72f73d69c0020236181e340bb8648fc1))
|
|
15
|
+
|
|
16
|
+
## [0.5.0](https://github.com/alesanfra/iterframes/compare/v0.4.0...v0.5.0) (2026-09-20)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### ⚠ BREAKING CHANGES
|
|
20
|
+
|
|
21
|
+
* `read_all` is removed; use `list(iterframes.read(...))`.
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* build wheels for Windows ([#4](https://github.com/alesanfra/iterframes/issues/4)) ([39702a5](https://github.com/alesanfra/iterframes/commit/39702a5b55333824956827c5cf23b50cf150fb22))
|
|
26
|
+
* read frames by number, in the style of decord ([#10](https://github.com/alesanfra/iterframes/issues/10)) ([6d0627e](https://github.com/alesanfra/iterframes/commit/6d0627e65d357667ba28f274c780c3a58da541cb))
|
|
27
|
+
* read frames in batches with read_batches ([#6](https://github.com/alesanfra/iterframes/issues/6)) ([136ae14](https://github.com/alesanfra/iterframes/commit/136ae14ad6cccfa3459f69518a610dd94957d4c1))
|
|
28
|
+
|
|
9
29
|
## [0.4.0](https://github.com/alesanfra/iterframes/compare/v0.3.0...v0.4.0) (2026-09-19)
|
|
10
30
|
|
|
11
31
|
The rewrite: static FFmpeg wheels, frames without copies, and hardware
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iterframes
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Classifier: Intended Audience :: Science/Research
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
7
|
+
Classifier: Programming Language :: Rust
|
|
8
|
+
Classifier: Topic :: Multimedia :: Video
|
|
9
|
+
Requires-Dist: numpy>=1.21
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Summary: Iterate over the frames of a video as NumPy arrays, decoded on a background thread while you process them
|
|
12
|
+
Keywords: video,decoder,ffmpeg,frames,numpy,video-reader,pytorch,deep-learning,computer-vision,nvdec,videotoolbox
|
|
13
|
+
Home-Page: https://github.com/alesanfra/iterframes
|
|
14
|
+
Author-email: Alessio Sanfratello <sanfra90@gmail.com>
|
|
15
|
+
License-Expression: LGPL-3.0-only
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
18
|
+
Project-URL: Changelog, https://github.com/alesanfra/iterframes/blob/main/CHANGELOG.md
|
|
19
|
+
Project-URL: Documentation, https://iterframes.readthedocs.io
|
|
20
|
+
Project-URL: Homepage, https://github.com/alesanfra/iterframes
|
|
21
|
+
Project-URL: Issues, https://github.com/alesanfra/iterframes/issues
|
|
22
|
+
Project-URL: Source, https://github.com/alesanfra/iterframes
|
|
23
|
+
|
|
24
|
+
# iterframes
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/iterframes/)
|
|
27
|
+
[](https://github.com/alesanfra/iterframes/actions/workflows/ci.yaml)
|
|
28
|
+
[](https://iterframes.readthedocs.io)
|
|
29
|
+
|
|
30
|
+
**Video frames as NumPy arrays, decoded while your code is busy with the
|
|
31
|
+
last one.**
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import iterframes
|
|
35
|
+
|
|
36
|
+
for frame in iterframes.read("video.mp4", height=224, width=224):
|
|
37
|
+
model(frame) # (224, 224, 3) uint8 RGB, resized by FFmpeg, no copy
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`model` runs on one frame while a Rust thread decodes the next ones. That
|
|
41
|
+
thread never takes the GIL, so decoding overlaps with your work instead of
|
|
42
|
+
adding to it, even when your code is pure Python. `pip install iterframes`
|
|
43
|
+
brings FFmpeg with it: nothing else to install, no system packages, no
|
|
44
|
+
`ffmpeg` binary to call.
|
|
45
|
+
|
|
46
|
+
The landing page at
|
|
47
|
+
[alesanfra.github.io/iterframes](https://alesanfra.github.io/iterframes/)
|
|
48
|
+
shows what the decoder does while your loop runs. The documentation is at
|
|
49
|
+
[iterframes.readthedocs.io](https://iterframes.readthedocs.io): the
|
|
50
|
+
[reference](https://iterframes.readthedocs.io/en/latest/reference/)
|
|
51
|
+
documents every argument and error, and the
|
|
52
|
+
[development guide](https://iterframes.readthedocs.io/en/latest/development/)
|
|
53
|
+
covers building from source.
|
|
54
|
+
|
|
55
|
+
## Numbers
|
|
56
|
+
|
|
57
|
+
One video, 901 frames of 480x270 H.264, decoded and converted to RGB,
|
|
58
|
+
best of seven runs on an Apple silicon Mac with the bundled FFmpeg 9.0.2.
|
|
59
|
+
The benchmarks are in
|
|
60
|
+
[`tests/test_benchmark.py`](https://github.com/alesanfra/iterframes/blob/main/tests/test_benchmark.py);
|
|
61
|
+
run them on your own videos before believing them.
|
|
62
|
+
|
|
63
|
+
| | Time |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| Every frame, iterframes | **0.046 s** |
|
|
66
|
+
| Every frame, PyAV, same decode and conversion | 0.208 s |
|
|
67
|
+
| 32 frames at random positions | 0.036 s |
|
|
68
|
+
| First 10 frames (`stop=10`) | 0.003 s |
|
|
69
|
+
| Second half (`start=450`) | 0.028 s |
|
|
70
|
+
|
|
71
|
+
In the overlap benchmark, which upscales the same video to 1080p, adding
|
|
72
|
+
per-frame work as expensive as decoding took 16% longer in total, not
|
|
73
|
+
twice as long: the decoding had already happened.
|
|
74
|
+
|
|
75
|
+
## What you get
|
|
76
|
+
|
|
77
|
+
- **Frames without copies.** RGB `uint8` arrays of shape
|
|
78
|
+
`(height, width, 3)`, straight out of FFmpeg's buffers.
|
|
79
|
+
- **Batches as one array.** `read_batches` decodes into a single
|
|
80
|
+
`(batch, height, width, 3)` block, ready for a model.
|
|
81
|
+
- **Resizing while decoding**, not a `cv2.resize` afterwards.
|
|
82
|
+
- **Random access.** Read frames by number, without decoding the rest.
|
|
83
|
+
- **Hardware decoding** on NVIDIA GPUs (NVDEC) and Apple silicon
|
|
84
|
+
(VideoToolbox), in the wheels. On NVIDIA the frames can stay on the GPU
|
|
85
|
+
for PyTorch.
|
|
86
|
+
- **Wheels** for Linux (x86_64, aarch64), macOS (Apple silicon), and
|
|
87
|
+
Windows, for every CPython from 3.11 on.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# Batches of 12 frames, decoded into one (12, 224, 224, 3) array
|
|
91
|
+
for batch in iterframes.read_batches("video.mp4", 12, height=224, width=224):
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
# Stop whenever you like; the decoder stops with the loop
|
|
95
|
+
for index, frame in enumerate(iterframes.read("video.mp4")):
|
|
96
|
+
if index == 100:
|
|
97
|
+
break
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Random access
|
|
101
|
+
|
|
102
|
+
Sample clips for training, or grab a thumbnail, without reading the whole
|
|
103
|
+
file:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
clip = list(iterframes.read("video.mp4", frames=[0, 30, 60]))
|
|
107
|
+
every_fifth = iterframes.read("video.mp4", start=100, stop=200, step=5)
|
|
108
|
+
last = next(iterframes.read("video.mp4", frames=[-1]))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Both work with `read_batches`. Negative numbers count from the end, and
|
|
112
|
+
frame `n` is the one `read` yields `n`-th. iterframes indexes the file
|
|
113
|
+
once, without decoding it, then decodes each frame from the key frame
|
|
114
|
+
before it; frames asked for in order cost no more than reading the video
|
|
115
|
+
straight through. See
|
|
116
|
+
[Reading frames by number](https://iterframes.readthedocs.io/en/latest/reference/#reading-frames-by-number).
|
|
117
|
+
|
|
118
|
+
When a frame nearby will do, `approximate` reads the key frame closest to
|
|
119
|
+
each of `frames`, which costs one decoded frame instead of the frames from
|
|
120
|
+
the key frame on:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# The nearest key frame within 5 frames, else the frame itself.
|
|
124
|
+
frames = list(iterframes.read("video.mp4", frames=[100, 200, 300], approximate=5))
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
See [Approximate frames](https://iterframes.readthedocs.io/en/latest/reference/#approximate-frames).
|
|
128
|
+
|
|
129
|
+
## Hardware decoding
|
|
130
|
+
|
|
131
|
+
Pass `device` to decode on a GPU instead of the CPU, which then stays free
|
|
132
|
+
for your model. The names are PyTorch's:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# NVIDIA GPU on Linux and Windows; with height and width, the GPU resizes too
|
|
136
|
+
for frame in iterframes.read("video.mp4", height=224, width=224, device="cuda"):
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
# Apple silicon (VideoToolbox)
|
|
140
|
+
for frame in iterframes.read("video.mp4", device="mps"):
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
# Whatever the machine has, else the CPU
|
|
144
|
+
for frame in iterframes.read("video.mp4", device="auto"):
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
print(iterframes.DEVICES) # ('cpu', 'mps') on a Mac
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The frames still arrive as NumPy arrays in memory. A GPU saves CPU time
|
|
151
|
+
but is not always faster than the CPU decoder, so measure both; see
|
|
152
|
+
[Hardware decoding](https://iterframes.readthedocs.io/en/latest/reference/#hardware-decoding).
|
|
153
|
+
|
|
154
|
+
With an NVIDIA GPU, `on_device=True` keeps the frames on it, in NV12, for
|
|
155
|
+
PyTorch and other libraries to take without a copy:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import torch
|
|
159
|
+
|
|
160
|
+
for frame in iterframes.read("video.mp4", device="cuda", on_device=True):
|
|
161
|
+
y = torch.from_dlpack(frame.y) # (height, width) uint8, on the GPU
|
|
162
|
+
uv = torch.from_dlpack(frame.uv) # (height / 2, width / 2, 2)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
[Frames on the GPU](https://iterframes.readthedocs.io/en/latest/reference/#frames-on-the-gpu) shows how to
|
|
166
|
+
convert them to RGB there.
|
|
167
|
+
|
|
168
|
+
## Compared with OpenCV, decord, and PyAV
|
|
169
|
+
|
|
170
|
+
| | iterframes | OpenCV `VideoCapture` | decord | PyAV |
|
|
171
|
+
| --- | --- | --- | --- | --- |
|
|
172
|
+
| Decoding runs | Ahead of your code, on a background thread | When you call `read()` | When you index the reader | When you ask for the next frame |
|
|
173
|
+
| Pixels | RGB | BGR | RGB | Any format FFmpeg supports |
|
|
174
|
+
| Resize while decoding | Yes | No, with `cv2.resize` after | Yes | Yes, with `reformat` |
|
|
175
|
+
| Batches as one array | Yes, `read_batches` | No | Yes, `get_batch` | No |
|
|
176
|
+
| Seeking and random access | Yes, `frames=[...]` or `start`, `stop`, `step` | Yes | Yes, fast | Yes |
|
|
177
|
+
| Audio, encoding, muxing | No | Encoding with `VideoWriter` | Audio reading | Yes |
|
|
178
|
+
| Hardware decoding | NVIDIA, Apple silicon, in the wheels | Depends on the build and backend | NVIDIA, when built from source | Depends on the build |
|
|
179
|
+
| Latest wheels | Linux, macOS, Windows, CPython 3.11+ | Linux, macOS, Windows | x86_64 only, last release in 2021 | Linux, macOS, Windows |
|
|
180
|
+
|
|
181
|
+
Pick iterframes to read videos into a model, in order or by frame number,
|
|
182
|
+
and keep decoding out of your loop's way. Pick PyAV when you need the rest
|
|
183
|
+
of FFmpeg: audio, encoding, streams, or precise control over the decoder.
|
|
184
|
+
OpenCV is the natural choice when the rest of the pipeline already uses
|
|
185
|
+
it.
|
|
186
|
+
|
|
187
|
+
## Installation
|
|
188
|
+
|
|
189
|
+
```console
|
|
190
|
+
pip install iterframes
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The wheels bundle FFmpeg and work on any CPython from 3.11 on, on Linux
|
|
194
|
+
(x86_64, aarch64), macOS (Apple silicon), and Windows (x86_64). Other
|
|
195
|
+
platforms build from source, FFmpeg included.
|
|
196
|
+
|
|
197
|
+
## Contributing
|
|
198
|
+
|
|
199
|
+
Contributions are welcome. The
|
|
200
|
+
[development guide](https://iterframes.readthedocs.io/en/latest/development/#help-wanted)
|
|
201
|
+
lists features that are waiting for someone to build them.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
iterframes is released under the [LGPL-3.0](https://github.com/alesanfra/iterframes/blob/main/LICENSE). The wheels include
|
|
206
|
+
[FFmpeg](https://ffmpeg.org/), built under the LGPL, and
|
|
207
|
+
[dav1d](https://code.videolan.org/videolan/dav1d), under the BSD 2-clause
|
|
208
|
+
license.
|
|
209
|
+
|