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.
Files changed (56) hide show
  1. iterframes-0.6.0/.github/scripts/check_site.py +50 -0
  2. {iterframes-0.4.0 → iterframes-0.6.0}/.github/workflows/ci.yaml +79 -1
  3. iterframes-0.6.0/.github/workflows/pages.yaml +61 -0
  4. iterframes-0.6.0/.release-please-manifest.json +3 -0
  5. {iterframes-0.4.0 → iterframes-0.6.0}/AGENTS.md +69 -9
  6. {iterframes-0.4.0 → iterframes-0.6.0}/CHANGELOG.md +20 -0
  7. {iterframes-0.4.0 → iterframes-0.6.0}/Cargo.lock +1 -1
  8. {iterframes-0.4.0 → iterframes-0.6.0}/Cargo.toml +1 -1
  9. iterframes-0.6.0/PKG-INFO +209 -0
  10. iterframes-0.6.0/README.md +185 -0
  11. {iterframes-0.4.0 → iterframes-0.6.0}/build.rs +17 -1
  12. {iterframes-0.4.0 → iterframes-0.6.0}/docs/development.md +34 -4
  13. {iterframes-0.4.0 → iterframes-0.6.0}/docs/index.md +23 -1
  14. {iterframes-0.4.0 → iterframes-0.6.0}/docs/reference.md +107 -13
  15. iterframes-0.6.0/iterframes/__init__.py +163 -0
  16. {iterframes-0.4.0 → iterframes-0.6.0}/pyproject.toml +4 -1
  17. {iterframes-0.4.0 → iterframes-0.6.0}/scripts/build-ffmpeg.sh +46 -6
  18. iterframes-0.6.0/src/decoder.rs +671 -0
  19. {iterframes-0.4.0 → iterframes-0.6.0}/src/ffmpeg.rs +202 -25
  20. {iterframes-0.4.0 → iterframes-0.6.0}/src/lib.rs +196 -42
  21. iterframes-0.6.0/tests/conftest.py +57 -0
  22. iterframes-0.6.0/tests/test_benchmark.py +136 -0
  23. {iterframes-0.4.0 → iterframes-0.6.0}/tests/test_read.py +107 -15
  24. iterframes-0.6.0/tests/test_select.py +394 -0
  25. iterframes-0.6.0/web/README.md +123 -0
  26. iterframes-0.6.0/web/assets/favicon.svg +5 -0
  27. iterframes-0.6.0/web/assets/fonts/jakarta-latin.woff2 +0 -0
  28. iterframes-0.6.0/web/assets/fonts/jetbrains-mono-latin.woff2 +0 -0
  29. iterframes-0.6.0/web/assets/frames.webp +0 -0
  30. iterframes-0.6.0/web/assets/og.png +0 -0
  31. iterframes-0.6.0/web/assets/scenes.webp +0 -0
  32. iterframes-0.6.0/web/index.html +532 -0
  33. iterframes-0.6.0/web/main.js +467 -0
  34. iterframes-0.6.0/web/og-card.html +51 -0
  35. iterframes-0.6.0/web/styles.css +730 -0
  36. iterframes-0.4.0/.release-please-manifest.json +0 -3
  37. iterframes-0.4.0/PKG-INFO +0 -106
  38. iterframes-0.4.0/README.md +0 -82
  39. iterframes-0.4.0/iterframes/__init__.py +0 -88
  40. iterframes-0.4.0/src/decoder.rs +0 -183
  41. iterframes-0.4.0/tests/conftest.py +0 -39
  42. iterframes-0.4.0/tests/test_benchmark.py +0 -61
  43. {iterframes-0.4.0 → iterframes-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  44. {iterframes-0.4.0 → iterframes-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  45. {iterframes-0.4.0 → iterframes-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  46. {iterframes-0.4.0 → iterframes-0.6.0}/.gitignore +0 -0
  47. {iterframes-0.4.0 → iterframes-0.6.0}/.pre-commit-config.yaml +0 -0
  48. {iterframes-0.4.0 → iterframes-0.6.0}/.readthedocs.yaml +0 -0
  49. {iterframes-0.4.0 → iterframes-0.6.0}/CLAUDE.md +0 -0
  50. {iterframes-0.4.0 → iterframes-0.6.0}/LICENSE +0 -0
  51. {iterframes-0.4.0 → iterframes-0.6.0}/mkdocs.yml +0 -0
  52. {iterframes-0.4.0 → iterframes-0.6.0}/release-please-config.json +0 -0
  53. {iterframes-0.4.0 → iterframes-0.6.0}/src/dlpack.rs +0 -0
  54. {iterframes-0.4.0 → iterframes-0.6.0}/tests/data/video_480x270.mp4 +0 -0
  55. {iterframes-0.4.0 → iterframes-0.6.0}/tests/data/video_av1_480x270.mp4 +0 -0
  56. {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
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.6.0"
3
+ }
@@ -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 `read_all`.
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 the `*_cuvid`
61
- decoders, which load the NVIDIA driver with dlopen and resize on the
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`, `read_all` |
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. `pre-commit run -a` covers the formatters and linters
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
@@ -96,7 +96,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
96
96
 
97
97
  [[package]]
98
98
  name = "iterframes"
99
- version = "0.4.0"
99
+ version = "0.6.0"
100
100
  dependencies = [
101
101
  "bindgen",
102
102
  "crossbeam-channel",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "iterframes"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.89"
6
6
  description = "Iterate over the frames of a video as NumPy arrays, decoded on a background thread while you process them"
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/iterframes.svg)](https://pypi.org/project/iterframes/)
27
+ [![CI](https://github.com/alesanfra/iterframes/actions/workflows/ci.yaml/badge.svg)](https://github.com/alesanfra/iterframes/actions/workflows/ci.yaml)
28
+ [![Documentation](https://readthedocs.org/projects/iterframes/badge/?version=latest)](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
+