iterframes 0.4.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 (35) hide show
  1. iterframes-0.4.0/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. iterframes-0.4.0/.github/ISSUE_TEMPLATE/feature_request.md +27 -0
  3. iterframes-0.4.0/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  4. iterframes-0.4.0/.github/workflows/ci.yaml +225 -0
  5. iterframes-0.4.0/.gitignore +158 -0
  6. iterframes-0.4.0/.pre-commit-config.yaml +35 -0
  7. iterframes-0.4.0/.readthedocs.yaml +21 -0
  8. iterframes-0.4.0/.release-please-manifest.json +3 -0
  9. iterframes-0.4.0/AGENTS.md +149 -0
  10. iterframes-0.4.0/CHANGELOG.md +80 -0
  11. iterframes-0.4.0/CLAUDE.md +5 -0
  12. iterframes-0.4.0/Cargo.lock +316 -0
  13. iterframes-0.4.0/Cargo.toml +31 -0
  14. iterframes-0.4.0/LICENSE +165 -0
  15. iterframes-0.4.0/PKG-INFO +106 -0
  16. iterframes-0.4.0/README.md +82 -0
  17. iterframes-0.4.0/build.rs +152 -0
  18. iterframes-0.4.0/docs/development.md +140 -0
  19. iterframes-0.4.0/docs/index.md +71 -0
  20. iterframes-0.4.0/docs/reference.md +219 -0
  21. iterframes-0.4.0/iterframes/__init__.py +88 -0
  22. iterframes-0.4.0/mkdocs.yml +67 -0
  23. iterframes-0.4.0/pyproject.toml +71 -0
  24. iterframes-0.4.0/release-please-config.json +14 -0
  25. iterframes-0.4.0/scripts/build-ffmpeg.sh +134 -0
  26. iterframes-0.4.0/src/decoder.rs +183 -0
  27. iterframes-0.4.0/src/dlpack.rs +142 -0
  28. iterframes-0.4.0/src/ffmpeg.rs +631 -0
  29. iterframes-0.4.0/src/lib.rs +370 -0
  30. iterframes-0.4.0/tests/conftest.py +39 -0
  31. iterframes-0.4.0/tests/data/video_480x270.mp4 +0 -0
  32. iterframes-0.4.0/tests/data/video_av1_480x270.mp4 +0 -0
  33. iterframes-0.4.0/tests/test_benchmark.py +61 -0
  34. iterframes-0.4.0/tests/test_read.py +232 -0
  35. iterframes-0.4.0/uv.lock +958 -0
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: Bug Report
3
+ about: Create a report to help us improve
4
+ title: '[BUG] '
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Bug Description
10
+
11
+ A clear and concise description of what the bug is.
12
+
13
+ ## Code Example
14
+
15
+ ```python
16
+ import iterframes
17
+
18
+ # Minimal code example that reproduces the issue
19
+ for frame in iterframes.read("video.mp4"):
20
+ ...
21
+ ```
22
+
23
+ ## Error Message
24
+
25
+ ```
26
+ Full error message and traceback if applicable
27
+ ```
28
+
29
+ ## Video
30
+
31
+ The container and codec of the video, as printed by
32
+ `ffprobe -hide_banner video.mp4`. Attach the file if you can share it.
33
+
34
+ ## Environment
35
+
36
+ - **OS:** [e.g., macOS 15, Ubuntu 24.04]
37
+ - **Python version:** [e.g., 3.13.2]
38
+ - **iterframes version:** [`iterframes.__version__`]
39
+ - **FFmpeg version:** [`iterframes.FFMPEG_VERSION`]
40
+ - **Installation method:** [wheel from PyPI, from source]
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: Feature Request
3
+ about: Suggest a new feature for this project
4
+ title: '[FEATURE] '
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Feature Description
10
+
11
+ A clear and concise description of the feature you'd like to see added.
12
+
13
+ ## Use Case
14
+
15
+ Describe the use case or problem this feature would solve.
16
+
17
+ ## Example Usage
18
+
19
+ ```python
20
+ import iterframes
21
+
22
+ # How you would like to use this feature
23
+ ```
24
+
25
+ ## Alternatives Considered
26
+
27
+ Have you considered any alternative solutions or features?
@@ -0,0 +1,10 @@
1
+ ## Description
2
+
3
+ <!-- What does this change and why? Link related issues, e.g. "Closes #123". -->
4
+
5
+ ## Checklist
6
+
7
+ - [ ] `cargo fmt` and `cargo clippy --all-targets -- -D warnings` pass
8
+ - [ ] `uv run --no-sync pre-commit run -a` passes
9
+ - [ ] Tests added or updated, and `uv run --no-sync pytest` passes
10
+ - [ ] Docs and `CHANGELOG.md` updated
@@ -0,0 +1,225 @@
1
+ # Wheel build matrix adapted from `maturin generate-ci github`. Every wheel
2
+ # links a static FFmpeg, which build.rs builds with scripts/build-ffmpeg.sh
3
+ # into build/ffmpeg, and is abi3, so a single wheel per platform covers
4
+ # every CPython from 3.11 on. Edit by hand.
5
+ name: CI
6
+
7
+ # Releases come from release-please, in this workflow: a push to main
8
+ # updates its release pull request, and merging that pull request creates
9
+ # the tag and a draft GitHub release, then the jobs below attach the
10
+ # wheels, publish them to PyPI, and publish the release.
11
+ on:
12
+ push:
13
+ branches:
14
+ - main
15
+ pull_request:
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ release-please:
23
+ runs-on: ubuntu-latest
24
+ if: ${{ github.event_name == 'push' }}
25
+ permissions:
26
+ contents: write
27
+ pull-requests: write
28
+ outputs:
29
+ release_created: ${{ steps.release.outputs.release_created }}
30
+ tag_name: ${{ steps.release.outputs.tag_name }}
31
+ version: ${{ steps.release.outputs.version }}
32
+ steps:
33
+ # Reads release-please-config.json and .release-please-manifest.json.
34
+ # With the default GITHUB_TOKEN, its tag and pull request start no
35
+ # other workflow, which is why the release jobs live here.
36
+ - uses: googleapis/release-please-action@v4
37
+ id: release
38
+
39
+ lint:
40
+ runs-on: ubuntu-24.04
41
+ steps:
42
+ - uses: actions/checkout@v7
43
+ - name: Install uv
44
+ uses: astral-sh/setup-uv@v10.1.0
45
+ - name: Install build tools
46
+ # Clippy runs build.rs, which builds FFmpeg and generates its
47
+ # bindings with libclang.
48
+ run: |
49
+ sudo apt-get update
50
+ sudo apt-get install -y --no-install-recommends \
51
+ libclang-dev nasm pkg-config
52
+ - name: Cache FFmpeg
53
+ uses: actions/cache@v6
54
+ with:
55
+ path: build/ffmpeg
56
+ key: ffmpeg-lint-${{ hashFiles('scripts/build-ffmpeg.sh') }}
57
+ - name: Check Rust formatting
58
+ run: cargo fmt --check
59
+ - name: Lint Rust
60
+ run: cargo clippy --all-targets -- -D warnings
61
+ - name: Lint Python
62
+ run: |
63
+ uvx ruff@0.16.8 check .
64
+ uvx ruff@0.16.8 format --check .
65
+
66
+ linux:
67
+ runs-on: ${{ matrix.platform.runner }}
68
+ strategy:
69
+ matrix:
70
+ platform:
71
+ # Native runners for each architecture: FFmpeg is compiled in the
72
+ # manylinux container, so there is no cross-compilation to set up.
73
+ - runner: ubuntu-24.04
74
+ target: x86_64
75
+ - runner: ubuntu-24.04-arm
76
+ target: aarch64
77
+ steps:
78
+ - uses: actions/checkout@v7
79
+ - name: Cache FFmpeg
80
+ uses: actions/cache@v6
81
+ with:
82
+ path: build/ffmpeg
83
+ key: ffmpeg-linux-${{ matrix.platform.target }}-${{ hashFiles('scripts/build-ffmpeg.sh') }}
84
+ - name: Build wheels
85
+ uses: PyO3/maturin-action@v1
86
+ with:
87
+ target: ${{ matrix.platform.target }}
88
+ args: --release --out dist
89
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
90
+ manylinux: 2_28
91
+ # libclang, for the bindings build.rs generates.
92
+ before-script-linux: dnf install -y clang-devel
93
+ - name: Upload wheels
94
+ uses: actions/upload-artifact@v7
95
+ with:
96
+ name: wheels-linux-${{ matrix.platform.target }}
97
+ path: dist
98
+ - name: Install uv
99
+ uses: astral-sh/setup-uv@v10.1.0
100
+ - name: Run tests
101
+ shell: bash
102
+ run: |
103
+ # The oldest and newest supported Pythons load the same abi3 wheel.
104
+ for python in 3.11 3.14; do
105
+ uv sync --frozen --no-install-project --python $python
106
+ uv pip install --reinstall-package iterframes dist/*.whl
107
+ uv run --no-sync pytest
108
+ done
109
+
110
+ macos:
111
+ runs-on: ${{ matrix.platform.runner }}
112
+ strategy:
113
+ matrix:
114
+ platform:
115
+ # Apple silicon only: Apple is phasing out Intel Macs.
116
+ - runner: macos-15
117
+ target: aarch64
118
+ env:
119
+ # Read by both the FFmpeg build and maturin, which tags the wheel.
120
+ MACOSX_DEPLOYMENT_TARGET: "11.0"
121
+ steps:
122
+ - uses: actions/checkout@v7
123
+ - name: Cache FFmpeg
124
+ uses: actions/cache@v6
125
+ with:
126
+ path: build/ffmpeg
127
+ key: ffmpeg-macos-${{ matrix.platform.target }}-${{ hashFiles('scripts/build-ffmpeg.sh') }}
128
+ - name: Install pkg-config
129
+ run: command -v pkg-config || brew install pkgconf
130
+ - name: Build wheels
131
+ uses: PyO3/maturin-action@v1
132
+ with:
133
+ target: ${{ matrix.platform.target }}
134
+ args: --release --out dist
135
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
136
+ - name: Upload wheels
137
+ uses: actions/upload-artifact@v7
138
+ with:
139
+ name: wheels-macos-${{ matrix.platform.target }}
140
+ path: dist
141
+ - name: Install uv
142
+ uses: astral-sh/setup-uv@v10.1.0
143
+ - name: Run tests
144
+ shell: bash
145
+ run: |
146
+ uv sync --frozen --no-install-project
147
+ uv pip install --reinstall-package iterframes dist/*.whl
148
+ uv run --no-sync pytest
149
+
150
+ sdist:
151
+ runs-on: ubuntu-latest
152
+ steps:
153
+ - uses: actions/checkout@v7
154
+ - name: Build sdist
155
+ uses: PyO3/maturin-action@v1
156
+ with:
157
+ command: sdist
158
+ args: --out dist
159
+ - name: Upload sdist
160
+ uses: actions/upload-artifact@v7
161
+ with:
162
+ name: wheels-sdist
163
+ path: dist
164
+
165
+ release:
166
+ name: Attach to the GitHub release
167
+ runs-on: ubuntu-latest
168
+ if: ${{ needs.release-please.outputs.release_created == 'true' }}
169
+ needs: [release-please, lint, linux, macos, sdist]
170
+ permissions:
171
+ # Used to sign the attestations
172
+ id-token: write
173
+ # Used to upload the files to the draft release
174
+ contents: write
175
+ # Used to generate artifact attestation
176
+ attestations: write
177
+ steps:
178
+ - uses: actions/download-artifact@v8
179
+ - name: Generate artifact attestation
180
+ uses: actions/attest@v4
181
+ with:
182
+ subject-path: 'wheels-*/*'
183
+ - name: Upload to the draft release
184
+ env:
185
+ GH_TOKEN: ${{ github.token }}
186
+ run: >
187
+ gh release upload ${{ needs.release-please.outputs.tag_name }}
188
+ wheels-*/* --clobber --repo ${{ github.repository }}
189
+
190
+ publish:
191
+ name: Publish to PyPI
192
+ runs-on: ubuntu-latest
193
+ needs: [release-please, release]
194
+ # Naming an environment records the run under Deployments, where the
195
+ # release can also be gated behind a review.
196
+ environment:
197
+ name: pypi
198
+ url: https://pypi.org/project/iterframes/${{ needs.release-please.outputs.version }}
199
+ permissions:
200
+ id-token: write
201
+ steps:
202
+ - uses: actions/download-artifact@v8
203
+ - name: Install uv
204
+ uses: astral-sh/setup-uv@v10.1.0
205
+ - name: Publish to PyPI
206
+ # Trusted publishing: PyPI mints a short-lived token from the job's
207
+ # OIDC identity, so no API token is stored. "always" fails loudly if
208
+ # that identity is missing instead of falling back.
209
+ run: uv publish --trusted-publishing always 'wheels-*/*'
210
+
211
+ publish-release:
212
+ name: Publish the GitHub release
213
+ runs-on: ubuntu-latest
214
+ # Last, so that the release only shows once its files are attached and
215
+ # on PyPI.
216
+ needs: [release-please, publish]
217
+ permissions:
218
+ contents: write
219
+ steps:
220
+ - name: Publish the draft release
221
+ env:
222
+ GH_TOKEN: ${{ github.token }}
223
+ run: >
224
+ gh release edit ${{ needs.release-please.outputs.tag_name }}
225
+ --draft=false --repo ${{ github.repository }}
@@ -0,0 +1,158 @@
1
+ ## IDE ##
2
+
3
+ .idea/
4
+ .vscode/
5
+
6
+ ## Rust related files ##
7
+
8
+ # Generated by Cargo
9
+ debug/
10
+ target/
11
+
12
+ # These are backup files generated by rustfmt
13
+ **/*.rs.bk
14
+
15
+ ## Python related files ##
16
+
17
+ # Byte-compiled / optimized / DLL files
18
+ __pycache__/
19
+ *.py[cod]
20
+ *$py.class
21
+
22
+ # C extensions
23
+ *.so
24
+
25
+ # Distribution / packaging
26
+ .Python
27
+ build/
28
+ develop-eggs/
29
+ dist/
30
+ downloads/
31
+ eggs/
32
+ .eggs/
33
+ lib/
34
+ lib64/
35
+ parts/
36
+ sdist/
37
+ var/
38
+ wheels/
39
+ wheelhouse/
40
+ share/python-wheels/
41
+ *.egg-info/
42
+ .installed.cfg
43
+ *.egg
44
+ MANIFEST
45
+
46
+ # PyInstaller
47
+ # Usually these files are written by a python script from a template
48
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
49
+ *.manifest
50
+ *.spec
51
+
52
+ # Installer logs
53
+ pip-log.txt
54
+ pip-delete-this-directory.txt
55
+
56
+ # Unit test / coverage reports
57
+ htmlcov/
58
+ .tox/
59
+ .nox/
60
+ .coverage
61
+ .coverage.*
62
+ .cache
63
+ nosetests.xml
64
+ coverage.xml
65
+ *.cover
66
+ *.py,cover
67
+ .hypothesis/
68
+ .pytest_cache/
69
+ cover/
70
+
71
+ # Translations
72
+ *.mo
73
+ *.pot
74
+
75
+ # Django stuff:
76
+ *.log
77
+ local_settings.py
78
+ db.sqlite3
79
+ db.sqlite3-journal
80
+
81
+ # Flask stuff:
82
+ instance/
83
+ .webassets-cache
84
+
85
+ # Scrapy stuff:
86
+ .scrapy
87
+
88
+ # Sphinx documentation
89
+ docs/_build/
90
+
91
+ # PyBuilder
92
+ .pybuilder/
93
+ target/
94
+
95
+ # Jupyter Notebook
96
+ .ipynb_checkpoints
97
+
98
+ # IPython
99
+ profile_default/
100
+ ipython_config.py
101
+
102
+ # pyenv
103
+ # For a library or package, you might want to ignore these files since the code is
104
+ # intended to run in multiple environments; otherwise, check them in:
105
+ # .python-version
106
+
107
+ # pipenv
108
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
109
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
110
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
111
+ # install all needed dependencies.
112
+ #Pipfile.lock
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .env
126
+ .venv
127
+ env/
128
+ venv/
129
+ ENV/
130
+ env.bak/
131
+ venv.bak/
132
+
133
+ # Spyder project settings
134
+ .spyderproject
135
+ .spyproject
136
+
137
+ # Rope project settings
138
+ .ropeproject
139
+
140
+ # mkdocs documentation
141
+ /site
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # MacOS files
158
+ .DS_Store
@@ -0,0 +1,35 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v6.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ # mkdocs.yml uses !!python/name tags for the Material emoji extension.
9
+ args: [--unsafe]
10
+ - id: check-toml
11
+ - id: check-added-large-files
12
+ - id: mixed-line-ending
13
+ args: [ --fix=lf ]
14
+ exclude: "make.bat"
15
+ # Run the local toolchain: the third-party Rust hook mirrors are stale.
16
+ - repo: local
17
+ hooks:
18
+ - id: cargo-fmt
19
+ name: cargo fmt
20
+ entry: cargo fmt --
21
+ language: system
22
+ types: [rust]
23
+ pass_filenames: false
24
+ - id: cargo-clippy
25
+ name: cargo clippy
26
+ entry: cargo clippy --all-targets -- -D warnings
27
+ language: system
28
+ types: [rust]
29
+ pass_filenames: false
30
+ - repo: https://github.com/astral-sh/ruff-pre-commit
31
+ rev: v0.16.8
32
+ hooks:
33
+ - id: ruff-format
34
+ - id: ruff-check
35
+ args: [--fix]
@@ -0,0 +1,21 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: "3.13"
7
+ jobs:
8
+ # The docs are plain Markdown, so only the docs group is installed and
9
+ # the extension, which would need FFmpeg, is not compiled.
10
+ pre_create_environment:
11
+ - asdf plugin add uv
12
+ - asdf install uv latest
13
+ - asdf global uv latest
14
+ create_environment:
15
+ - uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
16
+ install:
17
+ - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen --only-group docs
18
+
19
+ mkdocs:
20
+ configuration: mkdocs.yml
21
+ fail_on_warning: false
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -0,0 +1,149 @@
1
+ # AGENTS.md
2
+
3
+ Working notes for coding agents (and humans) touching this repository.
4
+
5
+ ## What this project is
6
+
7
+ `iterframes` is a Python extension module written in Rust with
8
+ [PyO3](https://pyo3.rs/) and built by [maturin](https://www.maturin.rs/).
9
+ It decodes videos with a static FFmpeg, through bindings that `build.rs`
10
+ generates and `src/ffmpeg.rs` wraps, and yields
11
+ the frames as NumPy arrays of RGB pixels.
12
+
13
+ The point of the project is a plain Python loop over the frames in which
14
+ decoding overlaps with expensive work on each frame, such as model
15
+ inference: while the caller processes one frame, a background thread
16
+ decodes the next ones. Keep that true, and say so in the docs.
17
+
18
+ The package uses maturin's mixed layout: the compiled module is installed
19
+ as `iterframes.iterframes`, and `iterframes/__init__.py` wraps its
20
+ `FrameReader` into `read` and `read_all`.
21
+
22
+ ## How decoding works
23
+
24
+ - `FrameReader::new` spawns a thread (`src/decoder.rs`) that demuxes,
25
+ decodes, and converts each frame to RGB24 with swscale, and sends it
26
+ through a bounded crossbeam channel of `prefetch_frames` slots.
27
+ - RGB frames are allocated with an alignment of 1, so their rows have no
28
+ padding and Python can use them as contiguous arrays.
29
+ - Every call into FFmpeg, and so almost all the `unsafe` code, lives in
30
+ `src/ffmpeg.rs`, behind small RAII types. Keep it that way: the rest of
31
+ the crate stays safe.
32
+ - `__next__` waits on the channel with the GIL released (`py.detach`) and
33
+ wraps the frame in a `Frame`, whose buffer protocol hands the pixels to
34
+ NumPy without a copy. The buffer protocol needs `abi3-py311`.
35
+ - The decoder thread never takes the GIL, so it keeps decoding while
36
+ Python code holds it. Never attach to Python there (no `Python::attach`,
37
+ no Python objects in `decoder.rs`); `test_benchmark.py` checks the
38
+ overlap.
39
+ - Errors travel through the channel and become Python exceptions in
40
+ `impl From<Error> for PyErr`. A closed channel means the end of the video.
41
+ - Dropping the reader closes the channel; the thread notices on its next
42
+ send and returns. Never `unwrap` a send.
43
+
44
+ ## FFmpeg
45
+
46
+ - Every build, local or CI, links the static FFmpeg that `build.rs`
47
+ builds with `scripts/build-ffmpeg.sh` into `build/ffmpeg` (or
48
+ `$ITERFRAMES_FFMPEG_DIR`) the first time, under a file lock, logging to
49
+ `build/ffmpeg.log`. The script is a no-op when `VERSION` in that
50
+ directory matches what it would build. There is no system FFmpeg mode.
51
+ - `build.rs` links the libraries through `pkg-config --static` and runs
52
+ bindgen on the headers, keeping only `av*_`/`sws_` items. Enums are
53
+ newtype structs: use `.0` for the raw value. Function-like macros such
54
+ as `AVERROR` are written by hand in the `sys` module of `src/ffmpeg.rs`.
55
+ - The build disables autodetection, so the wheel depends on libc and
56
+ system frameworks only; dav1d is added for AV1.
57
+ - Keep the build LGPL: never pass `--enable-gpl` or `--enable-nonfree`.
58
+ - Hardware decoding (`device="mps"` / `"cuda"`, PyTorch's names, mapped
59
+ 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
63
+ compiler-rt for `@available`, which `build.rs` links.
64
+ - NVDEC has never run on a GPU in this project: CI has none, and the
65
+ `cuda` tests skip when the device does not open. The same goes for
66
+ `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`).
69
+
70
+ ## Layout
71
+
72
+ | Path | Contents |
73
+ | --- | --- |
74
+ | `src/lib.rs` | PyO3 module: `Frame`, `FrameReader`, error mapping, module init |
75
+ | `src/decoder.rs` | Decoding thread |
76
+ | `src/ffmpeg.rs` | Safe wrappers over the FFmpeg calls the crate needs |
77
+ | `src/dlpack.rs` | DLPack capsules for the planes of `CudaFrame` |
78
+ | `iterframes/__init__.py` | `read`, `read_all` |
79
+ | `build.rs` | Builds and links FFmpeg, generates its bindings |
80
+ | `scripts/build-ffmpeg.sh` | Static FFmpeg and dav1d, run by `build.rs` |
81
+ | `tests/` | pytest suite; frames are compared with PyAV |
82
+ | `docs/` | MkDocs site published on Read the Docs |
83
+
84
+ ## Environment
85
+
86
+ Requires [uv](https://docs.astral.sh/uv/), a Rust toolchain, and what
87
+ FFmpeg needs to build: a C compiler, `make`, `curl`, `python3`,
88
+ `pkg-config`, libclang (see `docs/development.md`).
89
+
90
+ ```bash
91
+ uv venv -p 3.14 # once
92
+ uv sync --frozen # dev dependencies
93
+ uv run maturin develop --uv # build FFmpeg (first time) and the extension
94
+ ```
95
+
96
+ **`uv run` re-syncs the project by default and overwrites the module that
97
+ `maturin develop` just built.** Always run tests and scripts as:
98
+
99
+ ```bash
100
+ uv run --no-sync pytest
101
+ ```
102
+
103
+ ## Checks to run before proposing a change
104
+
105
+ ```bash
106
+ cargo fmt
107
+ cargo clippy --all-targets -- -D warnings
108
+ uv run maturin develop --uv
109
+ uv run --no-sync pytest
110
+ ```
111
+
112
+ 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
114
+ locally.
115
+
116
+ ## Docs
117
+
118
+ Three pages: `index.md` (overview), `reference.md` (API and errors),
119
+ `development.md`. Every example must match the behavior of a freshly
120
+ built module; check them instead of writing them from memory. Keep the
121
+ text short, in American English, with no performance claims that have not
122
+ been measured.
123
+
124
+ ## Conventions
125
+
126
+ - Commits follow [Conventional Commits](https://www.conventionalcommits.org/),
127
+ which release-please turns into versions and changelog entries.
128
+ - Rust: `cargo fmt` defaults, no `unwrap()` on anything reachable from
129
+ Python input (a panic surfaces as `PanicException`).
130
+ - Python: ruff with a 79-column limit.
131
+ - Comments explain why, not what.
132
+
133
+ ## Release
134
+
135
+ Version lives in `Cargo.toml` and is re-exported as
136
+ `iterframes.__version__`; `pyproject.toml` takes it from there. Never bump
137
+ it, write `CHANGELOG.md` entries, or push tags by hand: release-please
138
+ does, from the Conventional Commits, in the `release-please` job of
139
+ `.github/workflows/ci.yaml` (see "Releasing" in `docs/development.md`).
140
+ Tags are `vX.Y.Z`; before 1.0 a breaking change bumps the minor version.
141
+
142
+ Merging the release pull request creates the tag and a draft GitHub
143
+ release; the same run attaches the wheels (`release` job), uploads them
144
+ to PyPI (`publish`), and publishes the release (`publish-release`). The
145
+ jobs share one workflow because a tag pushed with the default
146
+ `GITHUB_TOKEN` starts no other workflow. The upload uses PyPI trusted
147
+ publishing, bound to this workflow file and the `pypi` environment name:
148
+ renaming either breaks publishing until the publisher is updated on
149
+ PyPI.