diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index ea077de8c..000000000 --- a/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.yml] -indent_size = 2 - -[Makefile] -indent_size = unset -indent_style = tab diff --git a/.github/ISSUE_TEMPLATE/build-bug-report.md b/.github/ISSUE_TEMPLATE/build-bug-report.md index 9f327e5de..298424330 100644 --- a/.github/ISSUE_TEMPLATE/build-bug-report.md +++ b/.github/ISSUE_TEMPLATE/build-bug-report.md @@ -65,7 +65,7 @@ I have done the following: - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://gitter.im/PyAV-Org) +- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) - [ ] ... and waited 72 hours for a response. diff --git a/.github/ISSUE_TEMPLATE/runtime-bug-report.md b/.github/ISSUE_TEMPLATE/runtime-bug-report.md index b29b11266..49fec3088 100644 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ b/.github/ISSUE_TEMPLATE/runtime-bug-report.md @@ -65,7 +65,7 @@ I have done the following: - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://gitter.im/PyAV-Org) +- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) - [ ] ... and waited 72 hours for a response. diff --git a/.github/ISSUE_TEMPLATE/user-help.md b/.github/ISSUE_TEMPLATE/user-help.md index 62c3e5c16..46d6776f6 100644 --- a/.github/ISSUE_TEMPLATE/user-help.md +++ b/.github/ISSUE_TEMPLATE/user-help.md @@ -43,7 +43,7 @@ I have done the following: - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://gitter.im/PyAV-Org) +- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) - [ ] ... and waited 72 hours for a response. diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml deleted file mode 100644 index d3ea994cf..000000000 --- a/.github/workflows/issues.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: issues -on: - schedule: - - cron: '30 1 * * *' - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v5 - with: - stale-issue-label: stale - stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' - days-before-stale: 120 - days-before-close: 14 - days-before-pr-stale: -1 - days-before-pr-close: -1 - operations-per-run: 60 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3cd077ade..d93fde792 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,52 +1,35 @@ name: tests on: push: + branches: main paths-ignore: - '**.md' - '**.rst' - '**.txt' pull_request: + branches: main paths-ignore: - '**.md' - '**.rst' - '**.txt' jobs: style: - name: "${{ matrix.config.suite }}" runs-on: ubuntu-latest - strategy: - matrix: - config: - - {suite: black} - - {suite: flake8} - - {suite: isort} - - env: - PYAV_PYTHON: python3 - PYAV_LIBRARY: ffmpeg-4.3 # doesn't matter steps: - - uses: actions/checkout@v3 - name: Checkout + - name: Checkout + uses: actions/checkout@v4 - name: Python uses: actions/setup-python@v4 with: python-version: 3.8 - - name: Environment - run: env | sort - - name: Packages - run: | - . scripts/activate.sh - # A bit of a hack that we can get away with this. - python -m pip install ${{ matrix.config.suite }} + run: pip install -r tests/requirements.txt - - name: "${{ matrix.config.suite }}" - run: | - . scripts/activate.sh - ./scripts/test ${{ matrix.config.suite }} + - name: Linters + run: make lint nix: name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" @@ -55,18 +38,19 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.0", extras: true} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.0"} - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.1"} - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.0"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "5.0"} - - {os: macos-latest, python: 3.8, ffmpeg: "5.0"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} + - {os: macos-latest, python: 3.8, ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout - name: Python ${{ matrix.config.python }} @@ -120,7 +104,7 @@ jobs: if: matrix.config.extras run: | . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/test doctest + make -C docs test - name: Examples if: matrix.config.extras @@ -128,12 +112,6 @@ jobs: . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} scripts/test examples - - name: Source Distribution - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/test sdist - windows: name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" runs-on: ${{ matrix.config.os }} @@ -142,12 +120,14 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: 3.8, ffmpeg: "6.1"} + - {os: windows-latest, python: 3.8, ffmpeg: "6.0"} - {os: windows-latest, python: 3.8, ffmpeg: "5.1"} - {os: windows-latest, python: 3.8, ffmpeg: "5.0"} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Conda shell: bash @@ -161,21 +141,14 @@ jobs: pillow \ python=${{ matrix.config.python }} \ setuptools - if [[ "${{ matrix.config.ffmpeg }}" == "5.1" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-win_amd64.tar.gz - elif [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-win_amd64.tar.gz - else - exit 1 - fi - name: Build shell: bash run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ - python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX/Library + python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library + python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - name: Test shell: bash @@ -187,14 +160,14 @@ jobs: package-source: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.8 - name: Build source package run: | pip install cython - python scripts/fetch-vendor.py /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v3 @@ -221,13 +194,13 @@ jobs: - os: windows-latest arch: AMD64 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.8 - name: Set up QEMU if: matrix.os == 'ubuntu-latest' - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Install packages if: matrix.os == 'macos-latest' run: | @@ -237,8 +210,8 @@ jobs: env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb - CIBW_BEFORE_BUILD: pip install cython && python scripts/fetch-vendor.py /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: pip install cython && python scripts\fetch-vendor.py C:\cibw\vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.1.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename @@ -262,7 +235,7 @@ jobs: runs-on: ubuntu-latest needs: [package-source, package-wheel] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/download-artifact@v3 with: name: dist diff --git a/.gitignore b/.gitignore index bc6c09077..2e71c06bc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,11 +30,11 @@ /ipch /msvc-projects /src +/docs/_ffmpeg # Testing. *.spyderproject .idea -/.vagrant /sandbox /tests/assets /tests/samples diff --git a/AUTHORS.rst b/AUTHORS.rst index 94601edd3..ea72e7cf6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -6,41 +6,79 @@ All contributors (by number of commits): - Mike Boers ; `@mikeboers `_ * Jeremy Lainé ; `@jlaine `_ -* Mark Reid ; `@markreidvfx `_ -- Vidar Tonaas Fauske ; `@vidartf `_ -- Billy Shambrook ; `@billyshambrook `_ -- Casper van der Wel -- Tadas Dailyda +- Mark Reid ; `@markreidvfx `_ -* Xinran Xu ; `@xxr3376 `_ -* Dan Allan ; `@danielballan `_ -* Alireza Davoudi ; `@adavoudi `_ -* Moritz Kassner ; `@mkassner `_ -* Thomas A Caswell ; `@tacaswell `_ -* Ulrik Mikaelsson ; `@rawler `_ -* Wel C. van der -* Will Patera ; `@willpatera `_ +* Vidar Tonaas Fauske ; `@vidartf `_ +* Billy Shambrook ; `@billyshambrook `_ +* Casper van der Wel +* Philip de Nier +* Tadas Dailyda +* WyattBlue +* Justin Wong <46082645+uvjustin@users.noreply.github.com> -- rutsh +- Alba Mendez +- Xinran Xu ; `@xxr3376 `_ +- Dan Allan ; `@danielballan `_ +- Dave Johansen +- Mark Harfouche - Christoph Rackwitz -- Johannes Erdfelt -- Karl Litterfeldt ; `@litterfeldt `_ -- Martin Larralde -- Miles Kaufmann -- Radek Senfeld ; `@radek-senfeld `_ -- Ian Lee -- Arthur Barros -- Gemfield -- mephi42 -- Manuel Goacolou -- Ömer Sezgin Uğurlu -- Orivej Desh -- Brendan Long ; `@brendanlong `_ -- Tom Flanagan -- Tim O'Shea -- Tim Ahpee -- Jonas Tingeborn -- Vasiliy Kotov -- Koichi Akabe -- David Joy +- Alireza Davoudi ; `@adavoudi `_ +- Jonathan Drolet +- Moritz Kassner ; `@mkassner `_ +- Thomas A Caswell ; `@tacaswell `_ +- Ulrik Mikaelsson ; `@rawler `_ +- Wel C. van der +- Will Patera ; `@willpatera `_ + +* rutsh +* Felix Vollmer +* Santiago Castro +* Christian Clauss +* Ihor Liubymov +* Johannes Erdfelt +* Karl Litterfeldt ; `@litterfeldt `_ +* Martin Larralde +* Simon-Martin Schröder +* mephi42 +* Miles Kaufmann +* Pablo Prietz +* Radek Senfeld ; `@radek-senfeld `_ +* Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +* Marc Mueller <30130371+cdce8p@users.noreply.github.com> +* zzjjbb <31069326+zzjjbb@users.noreply.github.com> +* Hanz <40712686+HanzCEO@users.noreply.github.com> +* Ian Lee +* Ryan Huang +* Arthur Barros +* Carlos Ruiz +* David Plowman +* Maxime Desroches +* egao1980 +* Eric Kalosa-Kenyon +* Gemfield +* Jonathan Martin +* Johan Jeppsson Karlin +* Philipp Klaus +* Mattias Wadman +* Manuel Goacolou +* Julian Schweizer +* Ömer Sezgin Uğurlu +* Orivej Desh +* Philipp Krähenbühl +* ramoncaldeira +* Santiago Castro +* Kengo Sawatsu +* FirefoxMetzger +* Brendan Long ; `@brendanlong `_ +* Семён Марьясин +* Stephen.Y +* Tom Flanagan +* Tim O'Shea +* Tim Ahpee +* Jonas Tingeborn +* Pino Toscano +* Ulrik Mikaelsson +* Vasiliy Kotov +* Koichi Akabe +* David Joy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index af5416bce..a6d4950c5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,30 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v11.0.0 +------- + +Major: + +- Add support for FFmpeg 6.0, drop support for FFmpeg < 5.0. +- Add support for Python 3.12, drop support for Python < 3.8. +- Build binary wheels against libvpx 1.13.1 to fix CVE-2023-5217. +- Build binary wheels against FFmpeg 6.0. + +Features: + +- Add support for the `ENCODER_FLUSH` encoder flag (:issue:`1067`). +- Add VideoFrame ndarray operations for yuv444p/yuvj444p formats (:issue:`788`). +- Add setters for `AVFrame.dts`, `AVPacket.is_keyframe` and `AVPacket.is_corrupt` (:issue:`1179`). + +Fixes: + +- Fix build using Cython 3 (:issue:`1140`). +- Populate new streams with codec parameters (:issue:`1044`). +- Explicitly set `python_requires` to avoid installing on incompatible Python (:issue:`1057`). +- Make `AudioFifo.__repr__` safe before the first frame (:issue:`1130`). +- Guard input container members against use after closes (:issue:`1137`). + v10.0.0 ------- diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index cc8be9008..000000000 --- a/HACKING.rst +++ /dev/null @@ -1,59 +0,0 @@ -Hacking on PyAV -=============== - -The Goal --------- - -The goal of PyAV is to not only wrap FFmpeg in Python and provide complete access to the library for power users, but to make FFmpeg approachable without the need to understand all of the underlying mechanics. - - -Names and Structure -------------------- - -As much as reasonable, PyAV mirrors FFmpeg's structure and naming. Ideally, searching for documentation for ``CodecContext.bit_rate`` leads to ``AVCodecContext.bit_rate`` as well. - -We allow ourselves to depart from FFmpeg to make everything feel more consistent, e.g.: - -- we change a few names to make them more readable, by adding underscores, etc.; -- all of the audio classes are prefixed with ``Audio``, while some of the FFmpeg structs are prefixed with ``Sample`` (e.g. ``AudioFormat`` vs ``AVSampleFormat``). - -We will also sometimes duplicate APIs in order to provide both a low-level and high-level experience, e.g.: - -- Object flags are usually exposed as a :class:`av.enum.EnumFlag` (with FFmpeg names) under a ``flags`` attribute, **and** each flag is also a boolean attribute (with more Pythonic names). - - -Version Compatibility ---------------------- - -We currently support FFmpeg 4.0 through 4.2, on Python 3.5 through 3.8, on Linux, macOS, and Windows. We `continually test `_ these configurations. - -Differences are handled at compile time, in C, by checking against ``LIBAV*_VERSION_INT`` macros. We have not been able to perform this sort of checking in Cython as we have not been able to have it fully remove the code-paths, and so there are missing functions in newer FFmpeg's, and deprecated ones that emit compiler warnings in older FFmpeg's. - -Unfortunately, this means that PyAV is built for the existing FFmpeg, and must be rebuilt when FFmpeg is updated. - -We used to do this detection in small ``*.pyav.h`` headers in the ``include`` directory (and there are still some there as of writing), but the preferred method is to create ``*-shims.c`` files that are cimport-ed by the one module that uses them. - -You can use the same build system as continuous integration for local development:: - - # Prep the environment. - source scripts/activate.sh - - # Build FFmpeg. - ./scripts/build-deps - - # Build PyAV. - make - - # Run the tests. - make test - - -Code Formatting and Linting ---------------------------- - -``isort`` and ``flake8`` are integrated into the continuous integration, and are required to pass for code to be merged into develop. You can run these via ``scripts/test``:: - - ./scripts/test isort - ./scripts/test flake8 - - diff --git a/Makefile b/Makefile index 6cb954dea..9a0b0a95f 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PYAV_PYTHON ?= python PYTHON := $(PYAV_PYTHON) -.PHONY: default build cythonize clean clean-all info lint test fate-suite test-assets docs +.PHONY: default build clean fate-suite lint test default: build @@ -13,78 +13,21 @@ default: build build: CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug -cythonize: - $(PYTHON) setup.py cythonize - - - -wheel: build-mingw32 - $(PYTHON) setup.py bdist_wheel - -build-mingw32: - # before running, set PKG_CONFIG_PATH to the pkgconfig dir of the ffmpeg build. - # set PKG_CONFIG_PATH=D:\dev\3rd\media-autobuild_suite\local32\bin-video\ffmpegSHARED\lib\pkgconfig - CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace -c mingw32 - mv *.pyd av - - +clean: + - find av -name '*.so' -delete + - rm -rf build + - rm -rf sandbox + - rm -rf src + - make -C docs clean fate-suite: # Grab ALL of the samples from the ffmpeg site. rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - TESTSUITE=flake8 scripts/test - TESTSUITE=isort scripts/test + black --check av examples tests + flake8 av examples tests + isort --check-only --diff av examples tests test: $(PYTHON) setup.py test - - - -vagrant: - vagrant box list | grep -q precise32 || vagrant box add precise32 http://files.vagrantup.com/precise32.box - -vtest: - vagrant ssh -c /vagrant/scripts/vagrant-test - - - -tmp/ffmpeg-git: - @ mkdir -p tmp/ffmpeg-git - git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git tmp/ffmpeg-git - -tmp/Doxyfile: tmp/ffmpeg-git - cp tmp/ffmpeg-git/doc/Doxyfile $@ - echo "GENERATE_TAGFILE = ../tagfile.xml" >> $@ - -tmp/tagfile.xml: tmp/Doxyfile - cd tmp/ffmpeg-git; doxygen ../Doxyfile - -docs: tmp/tagfile.xml - PYTHONPATH=.. make -C docs html - -deploy-docs: docs - ./docs/upload docs - - - - -clean-build: - - rm -rf build - - find av -name '*.so' -delete - -clean-sandbox: - - rm -rf sandbox/201* - - rm sandbox/last - -clean-src: - - rm -rf src - -clean-docs: - - rm tmp/Doxyfile - - rm tmp/tagfile.xml - - make -C docs clean - -clean: clean-build clean-sandbox clean-src -clean-all: clean-build clean-sandbox clean-src clean-docs diff --git a/README.md b/README.md index 74eecefcb..2559d9b41 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ If you want to use your existing FFmpeg, the source version of PyAV is on [PyPI] pip install av --no-binary av ``` +Installing from source is not supported on Windows. + Alternative installation methods -------------------------------- @@ -40,10 +42,10 @@ conda install av -c conda-forge See the [Conda install][conda-install] docs to get started with (mini)Conda. -And if you want to build from the absolute source (for development or testing): +And if you want to build from the absolute source (POSIX only): ```bash -git clone git@github.com:PyAV-Org/PyAV +git clone https://github.com/PyAV-Org/PyAV.git cd PyAV source scripts/activate.sh @@ -54,6 +56,7 @@ pip install --upgrade -r tests/requirements.txt # Build PyAV. make +pip install . ``` --- @@ -65,9 +68,9 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [conda-badge]: https://img.shields.io/conda/vn/conda-forge/av.svg?colorB=CCB39A [conda]: https://anaconda.org/conda-forge/av [docs-badge]: https://img.shields.io/badge/docs-on%20pyav.org-blue.svg -[docs]: http://pyav.org/docs +[docs]: https://pyav.basswood-io.com [gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e -[gitter]: https://gitter.im/PyAV-Org +[gitter]: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im [pypi-badge]: https://img.shields.io/pypi/v/av.svg?colorB=CCB39A [pypi]: https://pypi.org/project/av @@ -75,6 +78,6 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [github-tests]: https://github.com/PyAV-Org/PyAV/actions?workflow=tests [github]: https://github.com/PyAV-Org/PyAV -[ffmpeg]: http://ffmpeg.org/ +[ffmpeg]: https://ffmpeg.org/ [conda-forge]: https://conda-forge.github.io/ [conda-install]: https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html diff --git a/av/__init__.pyi b/av/__init__.pyi new file mode 100644 index 000000000..493c62012 --- /dev/null +++ b/av/__init__.pyi @@ -0,0 +1,18 @@ +from av import error, logging + +from .audio.format import * +from .audio.frame import * +from .audio.layout import * +from .audio.resampler import * +from .codec.codec import * +from .container.core import * +from .container.input import * +from .container.output import * +from .error import * +from .filter import * +from .format import * +from .packet import * +from .video.format import * +from .video.frame import * + +__version__: str diff --git a/av/__main__.py b/av/__main__.py index b5718ba8b..4e5d62692 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -7,13 +7,11 @@ def main(): parser.add_argument("--version", action="store_true") args = parser.parse_args() - # --- - if args.version: import av import av._core - print("PyAV v" + av.__version__) + print(f"PyAV v{av.__version__}") by_config = {} for libname, config in sorted(av._core.library_meta.items()): @@ -22,14 +20,13 @@ def main(): by_config.setdefault( (config["configuration"], config["license"]), [] ).append((libname, config)) + for (config, license), libs in sorted(by_config.items()): print("library configuration:", config) print("library license:", license) for libname, config in libs: version = config["version"] - print( - "%-13s %3d.%3d.%3d" % (libname, version[0], version[1], version[2]) - ) + print(f"{libname:<13} {version[0]:3d}.{version[1]:3d}.{version[2]:3d}") if args.codecs: from av.codec.codec import dump_codecs diff --git a/av/_core.pyi b/av/_core.pyi new file mode 100644 index 000000000..4fbfe8525 --- /dev/null +++ b/av/_core.pyi @@ -0,0 +1,9 @@ +from typing import TypedDict + +class _Meta(TypedDict): + version: tuple[int, int, int] + configuration: str + license: str + +library_meta: dict[str, _Meta] +library_versions: dict[str, tuple[int, int, int]] diff --git a/av/_core.pyx b/av/_core.pyx index b2a6e83bd..4100c5857 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -1,6 +1,5 @@ cimport libav as lib - # Initialise libraries. lib.avformat_network_init() lib.avdevice_register_all() @@ -12,47 +11,49 @@ time_base = lib.AV_TIME_BASE cdef decode_version(v): if v < 0: return (-1, -1, -1) + cdef int major = (v >> 16) & 0xff cdef int minor = (v >> 8) & 0xff cdef int micro = (v) & 0xff + return (major, minor, micro) library_meta = { - 'libavutil': dict( + "libavutil": dict( version=decode_version(lib.avutil_version()), configuration=lib.avutil_configuration(), license=lib.avutil_license() ), - 'libavcodec': dict( + "libavcodec": dict( version=decode_version(lib.avcodec_version()), configuration=lib.avcodec_configuration(), license=lib.avcodec_license() ), - 'libavformat': dict( + "libavformat": dict( version=decode_version(lib.avformat_version()), configuration=lib.avformat_configuration(), license=lib.avformat_license() ), - 'libavdevice': dict( + "libavdevice": dict( version=decode_version(lib.avdevice_version()), configuration=lib.avdevice_configuration(), license=lib.avdevice_license() ), - 'libavfilter': dict( + "libavfilter": dict( version=decode_version(lib.avfilter_version()), configuration=lib.avfilter_configuration(), license=lib.avfilter_license() ), - 'libswscale': dict( + "libswscale": dict( version=decode_version(lib.swscale_version()), configuration=lib.swscale_configuration(), license=lib.swscale_license() ), - 'libswresample': dict( + "libswresample": dict( version=decode_version(lib.swresample_version()), configuration=lib.swresample_configuration(), license=lib.swresample_license() ), } -library_versions = {name: meta['version'] for name, meta in library_meta.items()} +library_versions = {name: meta["version"] for name, meta in library_meta.items()} diff --git a/av/about.py b/av/about.py index 9158871f9..282214405 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "10.0.0" +__version__ = "12.0.0rc1" diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi new file mode 100644 index 000000000..ed478a906 --- /dev/null +++ b/av/audio/codeccontext.pyi @@ -0,0 +1,16 @@ +from typing import Literal + +from av.codec.context import CodecContext + +from .format import AudioFormat +from .layout import AudioLayout + +class AudioCodecContext(CodecContext): + frame_size: int + sample_rate: int + rate: int + channels: int + channel_layout: int + layout: AudioLayout + format: AudioFormat + type: Literal["audio"] diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 8446fbcd0..1978759ae 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -8,7 +8,6 @@ from av.packet cimport Packet cdef class AudioCodecContext(CodecContext): - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): CodecContext._init(self, ptr, codec) @@ -54,69 +53,76 @@ cdef class AudioCodecContext(CodecContext): cdef AudioFrame aframe = frame aframe._init_user_attributes() - property frame_size: + @property + def frame_size(self): """ Number of samples per channel in an audio frame. :type: int """ - def __get__(self): return self.ptr.frame_size + return self.ptr.frame_size + - property sample_rate: + @property + def sample_rate(self): """ Sample rate of the audio data, in samples per second. :type: int """ - def __get__(self): - return self.ptr.sample_rate + return self.ptr.sample_rate - def __set__(self, int value): - self.ptr.sample_rate = value + @sample_rate.setter + def sample_rate(self, int value): + self.ptr.sample_rate = value - property rate: + @property + def rate(self): """Another name for :attr:`sample_rate`.""" - def __get__(self): - return self.sample_rate + return self.sample_rate - def __set__(self, value): - self.sample_rate = value + @rate.setter + def rate(self, value): + self.sample_rate = value # TODO: Integrate into AudioLayout. - property channels: - def __get__(self): - return self.ptr.channels - - def __set__(self, value): - self.ptr.channels = value - self.ptr.channel_layout = lib.av_get_default_channel_layout(value) - property channel_layout: - def __get__(self): - return self.ptr.channel_layout - - property layout: + @property + def channels(self): + return self.ptr.channels + + @channels.setter + def channels(self, value): + self.ptr.channels = value + self.ptr.channel_layout = lib.av_get_default_channel_layout(value) + @property + def channel_layout(self): + return self.ptr.channel_layout + + @property + def layout(self): """ The audio channel layout. :type: AudioLayout """ - def __get__(self): - return get_audio_layout(self.ptr.channels, self.ptr.channel_layout) + return get_audio_layout(self.ptr.channels, self.ptr.channel_layout) - def __set__(self, value): - cdef AudioLayout layout = AudioLayout(value) - self.ptr.channel_layout = layout.layout - self.ptr.channels = layout.nb_channels + @layout.setter + def layout(self, value): + cdef AudioLayout layout = AudioLayout(value) + self.ptr.channel_layout = layout.layout + self.ptr.channels = layout.nb_channels - property format: + @property + def format(self): """ The audio sample format. :type: AudioFormat """ - def __get__(self): - return get_audio_format(self.ptr.sample_fmt) + return get_audio_format(self.ptr.sample_fmt) - def __set__(self, value): - cdef AudioFormat format = AudioFormat(value) - self.ptr.sample_fmt = format.sample_fmt + @format.setter + def format(self, value): + cdef AudioFormat format = AudioFormat(value) + self.ptr.sample_fmt = format.sample_fmt diff --git a/av/audio/fifo.pxd b/av/audio/fifo.pxd index cf3a9dbec..0ace5e4b1 100644 --- a/av/audio/fifo.pxd +++ b/av/audio/fifo.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport int64_t, uint64_t cimport libav as lib +from libc.stdint cimport int64_t, uint64_t from av.audio.frame cimport AudioFrame diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 9b95d5770..83d9cc71d 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -3,18 +3,20 @@ from av.error cimport err_check cdef class AudioFifo: - """A simple audio sample FIFO (First In First Out) buffer.""" def __repr__(self): - return '' % ( - self.__class__.__name__, - self.samples, - self.sample_rate, - self.layout, - self.format, - id(self), - ) + try: + result = ( + f"" + ) + except AttributeError: + result = ( + f"" + ) + return result def __dealloc__(self): if self.ptr: @@ -37,7 +39,7 @@ cdef class AudioFifo: """ if frame is None: - raise TypeError('AudioFifo must be given an AudioFrame.') + raise TypeError("AudioFifo must be given an AudioFrame.") if not frame.ptr.nb_samples: return @@ -64,7 +66,7 @@ cdef class AudioFifo: ) if not self.ptr: - raise RuntimeError('Could not allocate AVAudioFifo.') + raise RuntimeError("Could not allocate AVAudioFifo.") # Make sure nothing changed. elif ( @@ -76,14 +78,16 @@ cdef class AudioFifo: frame._time_base.den != self.template._time_base.den )) ): - raise ValueError('Frame does not match AudioFifo parameters.') + raise ValueError("Frame does not match AudioFifo parameters.") # Assert that the PTS are what we expect. cdef int64_t expected_pts if self.pts_per_sample and frame.ptr.pts != lib.AV_NOPTS_VALUE: expected_pts = (self.pts_per_sample * self.samples_written) if frame.ptr.pts != expected_pts: - raise ValueError('Frame.pts (%d) != expected (%d); fix or set to None.' % (frame.ptr.pts, expected_pts)) + raise ValueError( + "Frame.pts (%d) != expected (%d); fix or set to None." % (frame.ptr.pts, expected_pts) + ) err_check(lib.av_audio_fifo_write( self.ptr, @@ -166,21 +170,22 @@ cdef class AudioFifo: frames.append(frame) else: break + return frames - property format: + @property + def format(self): """The :class:`.AudioFormat` of this FIFO.""" - def __get__(self): - return self.template.format - property layout: + return self.template.format + @property + def layout(self): """The :class:`.AudioLayout` of this FIFO.""" - def __get__(self): - return self.template.layout - property sample_rate: - def __get__(self): - return self.template.sample_rate + return self.template.layout + @property + def sample_rate(self): + return self.template.sample_rate - property samples: + @property + def samples(self): """Number of audio samples (per channel) in the buffer.""" - def __get__(self): - return lib.av_audio_fifo_size(self.ptr) if self.ptr else 0 + return lib.av_audio_fifo_size(self.ptr) if self.ptr else 0 diff --git a/av/audio/format.pyi b/av/audio/format.pyi new file mode 100644 index 000000000..bb4a1e603 --- /dev/null +++ b/av/audio/format.pyi @@ -0,0 +1,9 @@ +class AudioFormat: + name: str + bytes: int + bits: int + is_planar: bool + is_packed: bool + planar: AudioFormat + packed: AudioFormat + container_name: str diff --git a/av/audio/format.pyx b/av/audio/format.pyx index 4c7cd1cdc..608610781 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -1,7 +1,7 @@ import sys -cdef str container_format_postfix = 'le' if sys.byteorder == 'little' else 'be' +cdef str container_format_postfix = "le" if sys.byteorder == "little" else "be" cdef object _cinit_bypass_sentinel @@ -18,11 +18,9 @@ cdef AudioFormat get_audio_format(lib.AVSampleFormat c_format): cdef class AudioFormat: - """Descriptor of audio formats.""" def __cinit__(self, name): - if name is _cinit_bypass_sentinel: return @@ -33,7 +31,7 @@ cdef class AudioFormat: sample_fmt = lib.av_get_sample_fmt(name) if sample_fmt < 0: - raise ValueError('Not a sample format: %r' % name) + raise ValueError(f"Not a sample format: {name!r}") self._init(sample_fmt) @@ -41,72 +39,74 @@ cdef class AudioFormat: self.sample_fmt = sample_fmt def __repr__(self): - return '' % (self.name) + return f"" - property name: + @property + def name(self): """Canonical name of the sample format. >>> SampleFormat('s16p').name 's16p' """ - def __get__(self): - return lib.av_get_sample_fmt_name(self.sample_fmt) + return lib.av_get_sample_fmt_name(self.sample_fmt) - property bytes: + @property + def bytes(self): """Number of bytes per sample. >>> SampleFormat('s16p').bytes 2 """ - def __get__(self): - return lib.av_get_bytes_per_sample(self.sample_fmt) + return lib.av_get_bytes_per_sample(self.sample_fmt) - property bits: + @property + def bits(self): """Number of bits per sample. >>> SampleFormat('s16p').bits 16 """ - def __get__(self): - return lib.av_get_bytes_per_sample(self.sample_fmt) << 3 + return lib.av_get_bytes_per_sample(self.sample_fmt) << 3 - property is_planar: + @property + def is_planar(self): """Is this a planar format? Strictly opposite of :attr:`is_packed`. """ - def __get__(self): - return bool(lib.av_sample_fmt_is_planar(self.sample_fmt)) + return bool(lib.av_sample_fmt_is_planar(self.sample_fmt)) - property is_packed: + @property + def is_packed(self): """Is this a planar format? Strictly opposite of :attr:`is_planar`. """ - def __get__(self): - return not lib.av_sample_fmt_is_planar(self.sample_fmt) + return not lib.av_sample_fmt_is_planar(self.sample_fmt) - property planar: + @property + def planar(self): """The planar variant of this format. Is itself when planar: + >>> from av import AudioFormat as Format >>> fmt = Format('s16p') >>> fmt.planar is fmt True """ - def __get__(self): - if self.is_planar: - return self - return get_audio_format(lib.av_get_planar_sample_fmt(self.sample_fmt)) + if self.is_planar: + return self + return get_audio_format(lib.av_get_planar_sample_fmt(self.sample_fmt)) - property packed: + @property + def packed(self): """The packed variant of this format. Is itself when packed: @@ -116,31 +116,29 @@ cdef class AudioFormat: True """ - def __get__(self): - if self.is_packed: - return self - return get_audio_format(lib.av_get_packed_sample_fmt(self.sample_fmt)) + if self.is_packed: + return self + return get_audio_format(lib.av_get_packed_sample_fmt(self.sample_fmt)) - property container_name: + @property + def container_name(self): """The name of a :class:`ContainerFormat` which directly accepts this data. :raises ValueError: when planar, since there are no such containers. """ - def __get__(self): - - if self.is_planar: - raise ValueError('no planar container formats') - - if self.sample_fmt == lib.AV_SAMPLE_FMT_U8: - return 'u8' - elif self.sample_fmt == lib.AV_SAMPLE_FMT_S16: - return 's16' + container_format_postfix - elif self.sample_fmt == lib.AV_SAMPLE_FMT_S32: - return 's32' + container_format_postfix - elif self.sample_fmt == lib.AV_SAMPLE_FMT_FLT: - return 'f32' + container_format_postfix - elif self.sample_fmt == lib.AV_SAMPLE_FMT_DBL: - return 'f64' + container_format_postfix - - raise ValueError('unknown layout') + if self.is_planar: + raise ValueError("no planar container formats") + + if self.sample_fmt == lib.AV_SAMPLE_FMT_U8: + return "u8" + elif self.sample_fmt == lib.AV_SAMPLE_FMT_S16: + return "s16" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_S32: + return "s32" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_FLT: + return "f32" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_DBL: + return "f64" + container_format_postfix + + raise ValueError("unknown layout") diff --git a/av/audio/frame.pxd b/av/audio/frame.pxd index a438fe627..e7ee88591 100644 --- a/av/audio/frame.pxd +++ b/av/audio/frame.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport uint8_t, uint64_t cimport libav as lib +from libc.stdint cimport uint8_t, uint64_t from av.audio.format cimport AudioFormat from av.audio.layout cimport AudioLayout diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi new file mode 100644 index 000000000..777c3dd70 --- /dev/null +++ b/av/audio/frame.pyi @@ -0,0 +1,35 @@ +from typing import Any, Union + +import numpy as np + +from av.frame import Frame + +from .plane import AudioPlane + +format_dtypes: dict[str, str] +_SupportedNDarray = Union[ + np.ndarray[Any, np.dtype[np.float64]], # f8 + np.ndarray[Any, np.dtype[np.float32]], # f4 + np.ndarray[Any, np.dtype[np.int32]], # i4 + np.ndarray[Any, np.dtype[np.int16]], # i2 + np.ndarray[Any, np.dtype[np.uint8]], # u1 +] + +class AudioFrame(Frame): + planes: tuple[AudioPlane, ...] + samples: int + sample_rate: int + rate: int + + def __init__( + self, + format: str = "s16", + layout: str = "stereo", + samples: int = 0, + align: int = 1, + ) -> None: ... + @staticmethod + def from_ndarray( + array: _SupportedNDarray, format: str = "s16", layout: str = "stereo" + ) -> AudioFrame: ... + def to_ndarray(self) -> _SupportedNDarray: ... diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index b97d5e043..ac3638230 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -4,21 +4,25 @@ from av.audio.plane cimport AudioPlane from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape +import warnings + +from av.deprecation import AVDeprecationWarning + cdef object _cinit_bypass_sentinel format_dtypes = { - 'dbl': 'f8', - 'dblp': 'f8', - 'flt': 'f4', - 'fltp': 'f4', - 's16': 'i2', - 's16p': 'i2', - 's32': 'i4', - 's32p': 'i4', - 'u8': 'u1', - 'u8p': 'u1', + "dbl": "f8", + "dblp": "f8", + "flt": "f4", + "fltp": "f4", + "s16": "i2", + "s16p": "i2", + "s32": "i4", + "s32p": "i4", + "u8": "u1", + "u8p": "u1", } @@ -33,10 +37,9 @@ cdef AudioFrame alloc_audio_frame(): cdef class AudioFrame(Frame): - """A frame of audio.""" - def __cinit__(self, format='s16', layout='stereo', samples=0, align=1): + def __cinit__(self, format="s16", layout="stereo", samples=0, align=1): if format is _cinit_bypass_sentinel: return @@ -57,7 +60,6 @@ cdef class AudioFrame(Frame): self.ptr.channels = self.layout.nb_channels if self.layout.channels and nb_samples: - # Cleanup the old buffer. lib.av_freep(&self._buffer) @@ -91,19 +93,13 @@ cdef class AudioFrame(Frame): self.format = get_audio_format(self.ptr.format) def __repr__(self): - return '' % ( - self.__class__.__name__, - self.index, - self.pts, - self.samples, - self.rate, - self.layout.name, - self.format.name, - id(self), + return ( + f" 8: - raise ValueError('no layout with %d channels' % layout) + raise ValueError(f"no layout with {layout} channels") + c_layout = default_layouts[layout] elif isinstance(layout, str): c_layout = lib.av_get_channel_layout(layout) elif isinstance(layout, AudioLayout): c_layout = layout.layout else: - raise TypeError('layout must be str or int') + raise TypeError("layout must be str or int") if not c_layout: - raise ValueError('invalid channel layout %r' % layout) + raise ValueError(f"invalid channel layout: {layout}") self._init(c_layout) @@ -94,31 +91,30 @@ cdef class AudioLayout: self.channels = tuple(AudioChannel(self, i) for i in range(self.nb_channels)) def __repr__(self): - return '' % (self.__class__.__name__, self.name) + return f"" - property name: + @property + def name(self): """The canonical name of the audio layout.""" - def __get__(self): - cdef char out[32] - # Passing 0 as number of channels... fix this later? - lib.av_get_channel_layout_string(out, 32, 0, self.layout) - return out + cdef char out[32] + # Passing 0 as number of channels... fix this later? + lib.av_get_channel_layout_string(out, 32, 0, self.layout) + return out cdef class AudioChannel: - def __cinit__(self, AudioLayout layout, int index): self.channel = lib.av_channel_layout_extract_channel(layout.layout, index) def __repr__(self): - return '' % (self.__class__.__name__, self.name, self.description) + return f"" - property name: + @property + def name(self): """The canonical name of the audio channel.""" - def __get__(self): - return lib.av_get_channel_name(self.channel) + return lib.av_get_channel_name(self.channel) - property description: + @property + def description(self): """A human description of the audio channel.""" - def __get__(self): - return channel_descriptions.get(self.name) + return channel_descriptions.get(self.name) diff --git a/av/audio/plane.pyi b/av/audio/plane.pyi new file mode 100644 index 000000000..64524dcdb --- /dev/null +++ b/av/audio/plane.pyi @@ -0,0 +1,4 @@ +from av.plane import Plane + +class AudioPlane(Plane): + buffer_size: int diff --git a/av/audio/resampler.pyi b/av/audio/resampler.pyi new file mode 100644 index 000000000..32861258e --- /dev/null +++ b/av/audio/resampler.pyi @@ -0,0 +1,20 @@ +from av.filter.graph import Graph + +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class AudioResampler: + rate: int + frame_size: int + format: AudioFormat + graph: Graph | None + + def __init__( + self, + format: AudioFormat | None = None, + layout: AudioLayout | None = None, + rate: int | None = None, + frame_size: int | None = None, + ) -> None: ... + def resample(self, frame: AudioFrame | None) -> list[AudioFrame]: ... diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 1214da317..89a5428e1 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -72,10 +72,14 @@ cdef class AudioResampler: # handle resampling with aformat filter # (similar to configure_output_audio_filter from ffmpeg) self.graph = av.filter.Graph() + extra_args = {} + if frame.time_base is not None: + extra_args["time_base"] = str(frame.time_base) abuffer = self.graph.add("abuffer", sample_rate=str(frame.sample_rate), sample_fmt=AudioFormat(frame.format).name, - channel_layout=frame.layout.name) + channel_layout=frame.layout.name, + **extra_args) aformat = self.graph.add("aformat", sample_rates=str(self.rate), sample_fmts=self.format.name, @@ -96,7 +100,7 @@ cdef class AudioResampler: frame.layout.layout != self.template.layout.layout or frame.sample_rate != self.template.rate ): - raise ValueError('Frame does not match AudioResampler setup.') + raise ValueError("Frame does not match AudioResampler setup.") self.graph.push(frame) @@ -106,7 +110,7 @@ cdef class AudioResampler: output.append(self.graph.pull()) except EOFError: break - except av.utils.AVError as e: + except av.AVError as e: if e.errno != errno.EAGAIN: raise break diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi new file mode 100644 index 000000000..cf4173759 --- /dev/null +++ b/av/audio/stream.pyi @@ -0,0 +1,16 @@ +from typing import Literal + +from av.packet import Packet +from av.stream import Stream + +from .codeccontext import AudioCodecContext +from .format import AudioFormat +from .frame import AudioFrame + +class AudioStream(Stream): + type: Literal["audio"] + format: AudioFormat + codec_context: AudioCodecContext + + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... # type: ignore[override] + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index 0a4f1523c..3c6b76fcf 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -1,12 +1,7 @@ cdef class AudioStream(Stream): - def __repr__(self): - return '' % ( - self.__class__.__name__, - self.index, - self.name, - self.rate, - self.layout.name, - self.format.name if self.format else None, - id(self), + form = self.format.name if self.format else None + return ( + f"" ) diff --git a/av/buffer.pyi b/av/buffer.pyi new file mode 100644 index 000000000..41db73e03 --- /dev/null +++ b/av/buffer.pyi @@ -0,0 +1,4 @@ +class Buffer: + buffer_size: int + buffer_ptr: int + def update(self, input: bytes) -> None: ... diff --git a/av/buffer.pyx b/av/buffer.pyx index 5affc81f0..20c7c4877 100644 --- a/av/buffer.pyx +++ b/av/buffer.pyx @@ -1,12 +1,10 @@ from cpython cimport PyBUF_WRITABLE, PyBuffer_FillInfo from libc.string cimport memcpy -from av import deprecation from av.bytesource cimport ByteSource, bytesource cdef class Buffer: - """A base class for PyAV objects which support the buffer protocol, such as :class:`.Packet` and :class:`.Plane`. @@ -23,7 +21,8 @@ cdef class Buffer: def __getbuffer__(self, Py_buffer *view, int flags): if flags & PyBUF_WRITABLE and not self._buffer_writable(): - raise ValueError('buffer is not writable') + raise ValueError("buffer is not writable") + PyBuffer_FillInfo(view, self, self._buffer_ptr(), self._buffer_size(), 0, flags) @property @@ -36,20 +35,6 @@ cdef class Buffer: """The memory address of the buffer.""" return self._buffer_ptr() - @deprecation.method - def to_bytes(self): - """Return the contents of this buffer as ``bytes``. - - This copies the entire contents; consider using something that uses - the `buffer protocol `_ - as that will be more efficient. - - This is largely for Python2, as Python 3 can do the same via - ``bytes(the_buffer)``. - - """ - return (self._buffer_ptr())[:self._buffer_size()] - def update(self, input): """Replace the data in this object with the given buffer. @@ -58,9 +43,12 @@ cdef class Buffer: """ if not self._buffer_writable(): - raise ValueError('buffer is not writable') + raise ValueError("buffer is not writable") + cdef ByteSource source = bytesource(input) cdef size_t size = self._buffer_size() + if source.length != size: - raise ValueError('got %d bytes; need %d bytes' % (source.length, size)) + raise ValueError(f"got {source.length} bytes; need {size} bytes") + memcpy(self._buffer_ptr(), source.ptr, size) diff --git a/av/bytesource.pyx b/av/bytesource.pyx index ec4138e72..9192c6d1a 100644 --- a/av/bytesource.pyx +++ b/av/bytesource.pyx @@ -2,12 +2,11 @@ from cpython.buffer cimport ( PyBUF_SIMPLE, PyBuffer_Release, PyObject_CheckBuffer, - PyObject_GetBuffer + PyObject_GetBuffer, ) cdef class ByteSource: - def __cinit__(self, owner): self.owner = owner @@ -28,7 +27,7 @@ cdef class ByteSource: self.length = self.view.len return - raise TypeError('expected bytes, bytearray or memoryview') + raise TypeError("expected bytes, bytearray or memoryview") def __dealloc__(self): if self.has_view: diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi new file mode 100644 index 000000000..572fa3149 --- /dev/null +++ b/av/codec/codec.pyi @@ -0,0 +1,68 @@ +from fractions import Fraction +from typing import Literal + +from av.audio.format import AudioFormat +from av.descriptor import Descriptor +from av.enum import EnumFlag +from av.video.format import VideoFormat + +from .context import CodecContext + +class Properties(EnumFlag): + NONE: int + INTRA_ONLY: int + LOSSY: int + LOSSLESS: int + REORDER: int + BITMAP_SUB: int + TEXT_SUB: int + +class Capabilities(EnumFlag): + NONE: int + DARW_HORIZ_BAND: int + DR1: int + HWACCEL: int + DELAY: int + SMALL_LAST_FRAME: int + HWACCEL_VDPAU: int + SUBFRAMES: int + EXPERIMENTAL: int + CHANNEL_CONF: int + NEG_LINESIZES: int + FRAME_THREADS: int + SLICE_THREADS: int + PARAM_CHANGE: int + AUTO_THREADS: int + VARIABLE_FRAME_SIZE: int + AVOID_PROBING: int + HARDWARE: int + HYBRID: int + ENCODER_REORDERED_OPAQUE: int + ENCODER_FLUSH: int + +class UnknownCodecError(ValueError): ... + +class Codec: + is_decoder: bool + descriptor: Descriptor + name: str + long_name: str + type: Literal["video", "audio", "data", "subtitle", "attachment"] + id: int + frame_rates: list[Fraction] | None + audio_rates: list[int] | None + video_formats: list[VideoFormat] | None + audio_formats: list[AudioFormat] | None + properties: Properties + capabilities: Capabilities + + def __init__(self, name: str, mode: Literal["r", "w"]) -> None: ... + def create(self) -> CodecContext: ... + +class codec_descriptor: + name: str + options: tuple[int, ...] + +codecs_available: set[str] + +def dump_codecs() -> None: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index a7002acc2..b350a8b4f 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -16,19 +16,19 @@ cdef Codec wrap_codec(const lib.AVCodec *ptr): return codec -Properties = define_enum('Properties', 'av.codec', ( - ('NONE', 0), - ('INTRA_ONLY', lib.AV_CODEC_PROP_INTRA_ONLY, +Properties = define_enum("Properties", "av.codec", ( + ("NONE", 0), + ("INTRA_ONLY", lib.AV_CODEC_PROP_INTRA_ONLY, """Codec uses only intra compression. Video and audio codecs only."""), - ('LOSSY', lib.AV_CODEC_PROP_LOSSY, + ("LOSSY", lib.AV_CODEC_PROP_LOSSY, """Codec supports lossy compression. Audio and video codecs only. Note: A codec may support both lossy and lossless compression modes."""), - ('LOSSLESS', lib.AV_CODEC_PROP_LOSSLESS, + ("LOSSLESS", lib.AV_CODEC_PROP_LOSSLESS, """Codec supports lossless compression. Audio and video codecs only."""), - ('REORDER', lib.AV_CODEC_PROP_REORDER, + ("REORDER", lib.AV_CODEC_PROP_REORDER, """Codec supports frame reordering. That is, the coded order (the order in which the encoded packets are output by the encoders / stored / input to the decoders) may be different from the presentation order of the corresponding @@ -36,24 +36,24 @@ Properties = define_enum('Properties', 'av.codec', ( For codecs that do not have this property set, PTS and DTS should always be equal."""), - ('BITMAP_SUB', lib.AV_CODEC_PROP_BITMAP_SUB, + ("BITMAP_SUB", lib.AV_CODEC_PROP_BITMAP_SUB, """Subtitle codec is bitmap based Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field."""), - ('TEXT_SUB', lib.AV_CODEC_PROP_TEXT_SUB, + ("TEXT_SUB", lib.AV_CODEC_PROP_TEXT_SUB, """Subtitle codec is text based. Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field."""), ), is_flags=True) -Capabilities = define_enum('Capabilities', 'av.codec', ( - ('NONE', 0), - ('DRAW_HORIZ_BAND', lib.AV_CODEC_CAP_DRAW_HORIZ_BAND, +Capabilities = define_enum("Capabilities", "av.codec", ( + ("NONE", 0), + ("DRAW_HORIZ_BAND", lib.AV_CODEC_CAP_DRAW_HORIZ_BAND, """Decoder can use draw_horiz_band callback."""), - ('DR1', lib.AV_CODEC_CAP_DR1, + ("DR1", lib.AV_CODEC_CAP_DR1, """Codec uses get_buffer() for allocating buffers and supports custom allocators. If not set, it might not use get_buffer() at all or use operations that assume the buffer was allocated by avcodec_default_get_buffer."""), - ('HWACCEL', 1 << 4), - ('DELAY', lib.AV_CODEC_CAP_DELAY, + ("HWACCEL", 1 << 4), + ("DELAY", lib.AV_CODEC_CAP_DELAY, """Encoder or decoder requires flushing with NULL input at the end in order to give the complete and correct output. @@ -75,11 +75,11 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( flag also means that the encoder must set the pts and duration for each output packet. If this flag is not set, the pts and duration will be determined by libavcodec from the input frame."""), - ('SMALL_LAST_FRAME', lib.AV_CODEC_CAP_SMALL_LAST_FRAME, + ("SMALL_LAST_FRAME", lib.AV_CODEC_CAP_SMALL_LAST_FRAME, """Codec can be fed a final frame with a smaller size. This can be used to prevent truncation of the last audio samples."""), - ('HWACCEL_VDPAU', 1 << 7), - ('SUBFRAMES', lib.AV_CODEC_CAP_SUBFRAMES, + ("HWACCEL_VDPAU", 1 << 7), + ("SUBFRAMES", lib.AV_CODEC_CAP_SUBFRAMES, """Codec can output multiple frames per AVPacket Normally demuxers return one frame at a time, demuxers which do not do are connected to a parser to split what they return into proper frames. @@ -89,25 +89,25 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( may return multiple frames in a packet. This has many disadvantages like prohibiting stream copy in many cases thus it should only be considered as a last resort."""), - ('EXPERIMENTAL', lib.AV_CODEC_CAP_EXPERIMENTAL, + ("EXPERIMENTAL", lib.AV_CODEC_CAP_EXPERIMENTAL, """Codec is experimental and is thus avoided in favor of non experimental encoders"""), - ('CHANNEL_CONF', lib.AV_CODEC_CAP_CHANNEL_CONF, + ("CHANNEL_CONF", lib.AV_CODEC_CAP_CHANNEL_CONF, """Codec should fill in channel configuration and samplerate instead of container"""), - ('NEG_LINESIZES', 1 << 11), - ('FRAME_THREADS', lib.AV_CODEC_CAP_FRAME_THREADS, + ("NEG_LINESIZES", 1 << 11), + ("FRAME_THREADS", lib.AV_CODEC_CAP_FRAME_THREADS, """Codec supports frame-level multithreading""",), - ('SLICE_THREADS', lib.AV_CODEC_CAP_SLICE_THREADS, + ("SLICE_THREADS", lib.AV_CODEC_CAP_SLICE_THREADS, """Codec supports slice-based (or partition-based) multithreading."""), - ('PARAM_CHANGE', lib.AV_CODEC_CAP_PARAM_CHANGE, + ("PARAM_CHANGE", lib.AV_CODEC_CAP_PARAM_CHANGE, """Codec supports changed parameters at any point."""), - ('AUTO_THREADS', lib.AV_CODEC_CAP_OTHER_THREADS, + ("AUTO_THREADS", lib.AV_CODEC_CAP_OTHER_THREADS, """Codec supports multithreading through a method other than slice- or frame-level multithreading. Typically this marks wrappers around multithreading-capable external libraries."""), - ('VARIABLE_FRAME_SIZE', lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE, + ("VARIABLE_FRAME_SIZE", lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE, """Audio encoder supports receiving a different number of samples in each call."""), - ('AVOID_PROBING', lib.AV_CODEC_CAP_AVOID_PROBING, + ("AVOID_PROBING", lib.AV_CODEC_CAP_AVOID_PROBING, """Decoder is not a preferred choice for probing. This indicates that the decoder is not a good choice for probing. It could for example be an expensive to spin up hardware decoder, @@ -115,19 +115,19 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( the stream. A decoder marked with this flag should only be used as last resort choice for probing."""), - ('HARDWARE', lib.AV_CODEC_CAP_HARDWARE, + ("HARDWARE", lib.AV_CODEC_CAP_HARDWARE, """Codec is backed by a hardware implementation. Typically used to identify a non-hwaccel hardware decoder. For information about hwaccels, use avcodec_get_hw_config() instead."""), - ('HYBRID', lib.AV_CODEC_CAP_HYBRID, + ("HYBRID", lib.AV_CODEC_CAP_HYBRID, """Codec is potentially backed by a hardware implementation, but not necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the implementation provides some sort of internal fallback."""), - ('ENCODER_REORDERED_OPAQUE', 1 << 20, # lib.AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, # FFmpeg 4.2 + ("ENCODER_REORDERED_OPAQUE", 1 << 20, # lib.AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, # FFmpeg 4.2 """This codec takes the reordered_opaque field from input AVFrames and returns it in the corresponding field in AVCodecContext after encoding."""), - ('ENCODER_FLUSH', 1 << 21, # lib.AV_CODEC_CAP_ENCODER_FLUSH # FFmpeg 4.3 + ("ENCODER_FLUSH", 1 << 21, # lib.AV_CODEC_CAP_ENCODER_FLUSH # FFmpeg 4.3 """This encoder can be flushed using avcodec_flush_buffers(). If this flag is not set, the encoder must be closed and reopened to ensure that no frames remain pending."""), @@ -160,19 +160,19 @@ cdef class Codec: """ - def __cinit__(self, name, mode='r'): + def __cinit__(self, name, mode="r"): if name is _cinit_sentinel: return - if mode == 'w': + if mode == "w": self.ptr = lib.avcodec_find_encoder_by_name(name) if not self.ptr: self.desc = lib.avcodec_descriptor_get_by_name(name) if self.desc: self.ptr = lib.avcodec_find_encoder(self.desc.id) - elif mode == 'r': + elif mode == "r": self.ptr = lib.avcodec_find_decoder_by_name(name) if not self.ptr: self.desc = lib.avcodec_descriptor_get_by_name(name) @@ -185,7 +185,7 @@ cdef class Codec: self._init(name) # Sanity check. - if (mode == 'w') != self.is_encoder: + if (mode == "w") != self.is_encoder: raise RuntimeError("Found codec does not match mode.", name, mode) cdef _init(self, name=None): @@ -196,30 +196,30 @@ cdef class Codec: if not self.desc: self.desc = lib.avcodec_descriptor_get(self.ptr.id) if not self.desc: - raise RuntimeError('No codec descriptor for %r.' % name) + raise RuntimeError("No codec descriptor for %r." % name) self.is_encoder = lib.av_codec_is_encoder(self.ptr) # Sanity check. if self.is_encoder and lib.av_codec_is_decoder(self.ptr): - raise RuntimeError('%s is both encoder and decoder.') + raise RuntimeError("%s is both encoder and decoder.") def create(self): """Create a :class:`.CodecContext` for this codec.""" from .context import CodecContext return CodecContext.create(self) - property is_decoder: - def __get__(self): - return not self.is_encoder + @property + def is_decoder(self): + return not self.is_encoder - property descriptor: - def __get__(self): return wrap_avclass(self.ptr.priv_class) + @property + def descriptor(self): return wrap_avclass(self.ptr.priv_class) - property name: - def __get__(self): return self.ptr.name or '' - property long_name: - def __get__(self): return self.ptr.long_name or '' + @property + def name(self): return self.ptr.name or "" + @property + def long_name(self): return self.ptr.long_name or "" @property def type(self): @@ -231,8 +231,8 @@ cdef class Codec: """ return lib.av_get_media_type_string(self.ptr.type) - property id: - def __get__(self): return self.ptr.id + @property + def id(self): return self.ptr.id @property def frame_rates(self): @@ -295,40 +295,40 @@ cdef class Codec: """Flag property of :class:`.Properties`""" return self.desc.props - intra_only = properties.flag_property('INTRA_ONLY') - lossy = properties.flag_property('LOSSY') # Defer to capability. - lossless = properties.flag_property('LOSSLESS') # Defer to capability. - reorder = properties.flag_property('REORDER') - bitmap_sub = properties.flag_property('BITMAP_SUB') - text_sub = properties.flag_property('TEXT_SUB') + intra_only = properties.flag_property("INTRA_ONLY") + lossy = properties.flag_property("LOSSY") # Defer to capability. + lossless = properties.flag_property("LOSSLESS") # Defer to capability. + reorder = properties.flag_property("REORDER") + bitmap_sub = properties.flag_property("BITMAP_SUB") + text_sub = properties.flag_property("TEXT_SUB") @Capabilities.property def capabilities(self): """Flag property of :class:`.Capabilities`""" return self.ptr.capabilities - draw_horiz_band = capabilities.flag_property('DRAW_HORIZ_BAND') - dr1 = capabilities.flag_property('DR1') - hwaccel = capabilities.flag_property('HWACCEL') - delay = capabilities.flag_property('DELAY') - small_last_frame = capabilities.flag_property('SMALL_LAST_FRAME') - hwaccel_vdpau = capabilities.flag_property('HWACCEL_VDPAU') - subframes = capabilities.flag_property('SUBFRAMES') - experimental = capabilities.flag_property('EXPERIMENTAL') - channel_conf = capabilities.flag_property('CHANNEL_CONF') - neg_linesizes = capabilities.flag_property('NEG_LINESIZES') - frame_threads = capabilities.flag_property('FRAME_THREADS') - slice_threads = capabilities.flag_property('SLICE_THREADS') - param_change = capabilities.flag_property('PARAM_CHANGE') - auto_threads = capabilities.flag_property('AUTO_THREADS') - variable_frame_size = capabilities.flag_property('VARIABLE_FRAME_SIZE') - avoid_probing = capabilities.flag_property('AVOID_PROBING') - # intra_only = capabilities.flag_property('INTRA_ONLY') # Dupes. - # lossless = capabilities.flag_property('LOSSLESS') # Dupes. - hardware = capabilities.flag_property('HARDWARE') - hybrid = capabilities.flag_property('HYBRID') - encoder_reordered_opaque = capabilities.flag_property('ENCODER_REORDERED_OPAQUE') - encoder_flush = capabilities.flag_property('ENCODER_FLUSH') + draw_horiz_band = capabilities.flag_property("DRAW_HORIZ_BAND") + dr1 = capabilities.flag_property("DR1") + hwaccel = capabilities.flag_property("HWACCEL") + delay = capabilities.flag_property("DELAY") + small_last_frame = capabilities.flag_property("SMALL_LAST_FRAME") + hwaccel_vdpau = capabilities.flag_property("HWACCEL_VDPAU") + subframes = capabilities.flag_property("SUBFRAMES") + experimental = capabilities.flag_property("EXPERIMENTAL") + channel_conf = capabilities.flag_property("CHANNEL_CONF") + neg_linesizes = capabilities.flag_property("NEG_LINESIZES") + frame_threads = capabilities.flag_property("FRAME_THREADS") + slice_threads = capabilities.flag_property("SLICE_THREADS") + param_change = capabilities.flag_property("PARAM_CHANGE") + auto_threads = capabilities.flag_property("AUTO_THREADS") + variable_frame_size = capabilities.flag_property("VARIABLE_FRAME_SIZE") + avoid_probing = capabilities.flag_property("AVOID_PROBING") + # intra_only = capabilities.flag_property("INTRA_ONLY") # Dupes. + # lossless = capabilities.flag_property("LOSSLESS") # Dupes. + hardware = capabilities.flag_property("HARDWARE") + hybrid = capabilities.flag_property("HYBRID") + encoder_reordered_opaque = capabilities.flag_property("ENCODER_REORDERED_OPAQUE") + encoder_flush = capabilities.flag_property("ENCODER_FLUSH") cdef get_codec_names(): @@ -352,7 +352,8 @@ codec_descriptor = wrap_avclass(lib.avcodec_get_class()) def dump_codecs(): """Print information about available codecs.""" - print '''Codecs: + print( + """Codecs: D..... = Decoding supported .E.... = Encoding supported ..V... = Video codec @@ -361,17 +362,17 @@ def dump_codecs(): ...I.. = Intra frame-only codec ....L. = Lossy compression .....S = Lossless compression - ------''' + ------""" + ) for name in sorted(codecs_available): - try: - e_codec = Codec(name, 'w') + e_codec = Codec(name, "w") except ValueError: e_codec = None try: - d_codec = Codec(name, 'r') + d_codec = Codec(name, "r") except ValueError: d_codec = None @@ -379,15 +380,18 @@ def dump_codecs(): codec = e_codec or d_codec try: - print ' %s%s%s%s%s%s %-18s %s' % ( - '.D'[bool(d_codec)], - '.E'[bool(e_codec)], - codec.type[0].upper(), - '.I'[codec.intra_only], - '.L'[codec.lossy], - '.S'[codec.lossless], - codec.name, - codec.long_name + print( + " %s%s%s%s%s%s %-18s %s" + % ( + ".D"[bool(d_codec)], + ".E"[bool(e_codec)], + codec.type[0].upper(), + ".I"[codec.intra_only], + ".L"[codec.lossy], + ".S"[codec.lossless], + codec.name, + codec.long_name, + ) ) except Exception as e: - print '...... %-18s ERROR: %s' % (codec.name, e) + print(f"...... {codec.name:<18} ERROR: {e}") diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 6cc8bd899..f247655ff 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport int64_t cimport libav as lib +from libc.stdint cimport int64_t from av.bytesource cimport ByteSource from av.codec.codec cimport Codec @@ -40,6 +40,7 @@ cdef class CodecContext: # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing # packets are bogus). It should take all info it needs from the context and/or stream. + cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) cdef _prepare_frames_for_encode(self, Frame frame) cdef _setup_encoded_packet(self, Packet) cdef _setup_decoded_frame(self, Frame, Packet) @@ -50,7 +51,6 @@ cdef class CodecContext: # resampling audio to a higher rate but with fixed size frames), and the # send/recv buffer may be limited to a single frame. Ergo, we need to flush # the buffer as often as possible. - cdef _send_frame_and_recv(self, Frame frame) cdef _recv_packet(self) cdef _send_packet_and_recv(self, Packet packet) cdef _recv_frame(self) diff --git a/av/codec/context.pyi b/av/codec/context.pyi new file mode 100644 index 000000000..ee81daae5 --- /dev/null +++ b/av/codec/context.pyi @@ -0,0 +1,80 @@ +from typing import Any, Literal + +from av.enum import EnumFlag, EnumItem +from av.packet import Packet + +from .codec import Codec + +class ThreadType(EnumFlag): + NONE: int + FRAME: int + SLICE: int + AUTO: int + +class SkipType(EnumItem): + NONE: int + DEFAULT: int + NONREF: int + BIDIR: int + NONINTRA: int + NONKEY: int + ALL: int + +class Flags(EnumFlag): + NONE: int + UNALIGNED: int + QSCALE: int + # 4MV + OUTPUT_CORRUPT: int + QPEL: int + DROPCHANGED: int + PASS1: int + PASS2: int + LOOP_FILTER: int + GRAY: int + PSNR: int + INTERLACED_DCT: int + LOW_DELAY: int + GLOBAL_HEADER: int + BITEXACT: int + AC_PRED: int + INTERLACED_ME: int + CLOSED_GOP: int + +class Flags2(EnumFlag): + NONE: int + FAST: int + NO_OUTPUT: int + LOCAL_HEADER: int + CHUNKS: int + IGNORE_CROP: int + SHOW_ALL: int + EXPORT_MVS: int + SKIP_MANUAL: int + RO_FLUSH_NOOP: int + +class CodecContext: + extradata: bytes | None + is_open: bool + is_encoder: bool + is_decoder: bool + name: str + type: Literal["video", "audio", "data", "subtitle", "attachment"] + profile: str | None + codec_tag: str + bit_rate: int | None + max_bit_rate: int | None + bit_rate_tolerance: int + thread_count: int + thread_type: Any + skip_frame: Any + + def open(self, strict: bool = True) -> None: ... + def close(self, strict: bool = True) -> None: ... + @staticmethod + def create( + codec: str | Codec, mode: Literal["r", "w"] | None = None + ) -> CodecContext: ... + def parse( + self, raw_input: bytes | bytearray | memoryview | None = None + ) -> list[Packet]: ... diff --git a/av/codec/context.pyx b/av/codec/context.pyx index bc8b35d57..e2557e702 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -1,9 +1,9 @@ import warnings +cimport libav as lib from libc.errno cimport EAGAIN from libc.stdint cimport uint8_t from libc.string cimport memcpy -cimport libav as lib from av.bytesource cimport ByteSource, bytesource from av.codec.codec cimport Codec, wrap_codec @@ -43,94 +43,94 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode return py_ctx -ThreadType = define_enum('ThreadType', __name__, ( - ('NONE', 0), - ('FRAME', lib.FF_THREAD_FRAME, +ThreadType = define_enum("ThreadType", __name__, ( + ("NONE", 0), + ("FRAME", lib.FF_THREAD_FRAME, """Decode more than one frame at once"""), - ('SLICE', lib.FF_THREAD_SLICE, + ("SLICE", lib.FF_THREAD_SLICE, """Decode more than one part of a single frame at once"""), - ('AUTO', lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, + ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, """Decode using both FRAME and SLICE methods."""), ), is_flags=True) -SkipType = define_enum('SkipType', __name__, ( - ('NONE', lib.AVDISCARD_NONE, +SkipType = define_enum("SkipType", __name__, ( + ("NONE", lib.AVDISCARD_NONE, """Discard nothing"""), - ('DEFAULT', lib.AVDISCARD_DEFAULT, + ("DEFAULT", lib.AVDISCARD_DEFAULT, """Discard useless packets like 0 size packets in AVI"""), - ('NONREF', lib.AVDISCARD_NONREF, + ("NONREF", lib.AVDISCARD_NONREF, """Discard all non reference"""), - ('BIDIR', lib.AVDISCARD_BIDIR, + ("BIDIR", lib.AVDISCARD_BIDIR, """Discard all bidirectional frames"""), - ('NONINTRA', lib.AVDISCARD_NONINTRA, + ("NONINTRA", lib.AVDISCARD_NONINTRA, """Discard all non intra frames"""), - ('NONKEY', lib.AVDISCARD_NONKEY, + ("NONKEY", lib.AVDISCARD_NONKEY, """Discard all frames except keyframes"""), - ('ALL', lib.AVDISCARD_ALL, + ("ALL", lib.AVDISCARD_ALL, """Discard all"""), )) -Flags = define_enum('Flags', __name__, ( - ('NONE', 0), - ('UNALIGNED', lib.AV_CODEC_FLAG_UNALIGNED, +Flags = define_enum("Flags", __name__, ( + ("NONE", 0), + ("UNALIGNED", lib.AV_CODEC_FLAG_UNALIGNED, """Allow decoders to produce frames with data planes that are not aligned to CPU requirements (e.g. due to cropping)."""), - ('QSCALE', lib.AV_CODEC_FLAG_QSCALE, + ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, """Use fixed qscale."""), - ('4MV', lib.AV_CODEC_FLAG_4MV, + ("4MV", lib.AV_CODEC_FLAG_4MV, """4 MV per MB allowed / advanced prediction for H.263."""), - ('OUTPUT_CORRUPT', lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, + ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, """Output even those frames that might be corrupted."""), - ('QPEL', lib.AV_CODEC_FLAG_QPEL, + ("QPEL", lib.AV_CODEC_FLAG_QPEL, """Use qpel MC."""), - ('DROPCHANGED', 1 << 5, + ("DROPCHANGED", 1 << 5, """Don't output frames whose parameters differ from first decoded frame in stream."""), - ('PASS1', lib.AV_CODEC_FLAG_PASS1, + ("PASS1", lib.AV_CODEC_FLAG_PASS1, """Use internal 2pass ratecontrol in first pass mode."""), - ('PASS2', lib.AV_CODEC_FLAG_PASS2, + ("PASS2", lib.AV_CODEC_FLAG_PASS2, """Use internal 2pass ratecontrol in second pass mode."""), - ('LOOP_FILTER', lib.AV_CODEC_FLAG_LOOP_FILTER, + ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, """loop filter."""), - ('GRAY', lib.AV_CODEC_FLAG_GRAY, + ("GRAY", lib.AV_CODEC_FLAG_GRAY, """Only decode/encode grayscale."""), - ('PSNR', lib.AV_CODEC_FLAG_PSNR, + ("PSNR", lib.AV_CODEC_FLAG_PSNR, """error[?] variables will be set during encoding."""), - ('INTERLACED_DCT', lib.AV_CODEC_FLAG_INTERLACED_DCT, + ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, """Use interlaced DCT."""), - ('LOW_DELAY', lib.AV_CODEC_FLAG_LOW_DELAY, + ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, """Force low delay."""), - ('GLOBAL_HEADER', lib.AV_CODEC_FLAG_GLOBAL_HEADER, + ("GLOBAL_HEADER", lib.AV_CODEC_FLAG_GLOBAL_HEADER, """Place global headers in extradata instead of every keyframe."""), - ('BITEXACT', lib.AV_CODEC_FLAG_BITEXACT, + ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, """Use only bitexact stuff (except (I)DCT)."""), - ('AC_PRED', lib.AV_CODEC_FLAG_AC_PRED, + ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, """H.263 advanced intra coding / MPEG-4 AC prediction"""), - ('INTERLACED_ME', lib.AV_CODEC_FLAG_INTERLACED_ME, + ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, """Interlaced motion estimation"""), - ('CLOSED_GOP', lib.AV_CODEC_FLAG_CLOSED_GOP), + ("CLOSED_GOP", lib.AV_CODEC_FLAG_CLOSED_GOP), ), is_flags=True) -Flags2 = define_enum('Flags2', __name__, ( - ('NONE', 0), - ('FAST', lib.AV_CODEC_FLAG2_FAST, +Flags2 = define_enum("Flags2", __name__, ( + ("NONE", 0), + ("FAST", lib.AV_CODEC_FLAG2_FAST, """Allow non spec compliant speedup tricks."""), - ('NO_OUTPUT', lib.AV_CODEC_FLAG2_NO_OUTPUT, + ("NO_OUTPUT", lib.AV_CODEC_FLAG2_NO_OUTPUT, """Skip bitstream encoding."""), - ('LOCAL_HEADER', lib.AV_CODEC_FLAG2_LOCAL_HEADER, + ("LOCAL_HEADER", lib.AV_CODEC_FLAG2_LOCAL_HEADER, """Place global headers at every keyframe instead of in extradata."""), - ('CHUNKS', lib.AV_CODEC_FLAG2_CHUNKS, + ("CHUNKS", lib.AV_CODEC_FLAG2_CHUNKS, """Input bitstream might be truncated at a packet boundaries instead of only at frame boundaries."""), - ('IGNORE_CROP', lib.AV_CODEC_FLAG2_IGNORE_CROP, + ("IGNORE_CROP", lib.AV_CODEC_FLAG2_IGNORE_CROP, """Discard cropping information from SPS."""), - ('SHOW_ALL', lib.AV_CODEC_FLAG2_SHOW_ALL, + ("SHOW_ALL", lib.AV_CODEC_FLAG2_SHOW_ALL, """Show all frames before the first keyframe"""), - ('EXPORT_MVS', lib.AV_CODEC_FLAG2_EXPORT_MVS, + ("EXPORT_MVS", lib.AV_CODEC_FLAG2_EXPORT_MVS, """Export motion vectors through frame side data"""), - ('SKIP_MANUAL', lib.AV_CODEC_FLAG2_SKIP_MANUAL, + ("SKIP_MANUAL", lib.AV_CODEC_FLAG2_SKIP_MANUAL, """Do not skip samples and export skip information as frame side data"""), - ('RO_FLUSH_NOOP', lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP, + ("RO_FLUSH_NOOP", lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP, """Do not reset ASS ReadOrder field on flush (subtitles decoding)"""), ), is_flags=True) @@ -145,16 +145,15 @@ cdef class CodecContext: def __cinit__(self, sentinel=None, *args, **kwargs): if sentinel is not _cinit_sentinel: - raise RuntimeError('Cannot instantiate CodecContext') + raise RuntimeError("Cannot instantiate CodecContext") self.options = {} self.stream_index = -1 # This is set by the container immediately. cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): - self.ptr = ptr if self.ptr.codec and codec and self.ptr.codec != codec: - raise RuntimeError('Wrapping CodecContext with mismatched codec.') + raise RuntimeError("Wrapping CodecContext with mismatched codec.") self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) # Set reasonable threading defaults. @@ -175,24 +174,24 @@ cdef class CodecContext: """Flag property of :class:`.Flags`.""" ) - unaligned = flags.flag_property('UNALIGNED') - qscale = flags.flag_property('QSCALE') - four_mv = flags.flag_property('4MV') - output_corrupt = flags.flag_property('OUTPUT_CORRUPT') - qpel = flags.flag_property('QPEL') - drop_changed = flags.flag_property('DROPCHANGED') - pass1 = flags.flag_property('PASS1') - pass2 = flags.flag_property('PASS2') - loop_filter = flags.flag_property('LOOP_FILTER') - gray = flags.flag_property('GRAY') - psnr = flags.flag_property('PSNR') - interlaced_dct = flags.flag_property('INTERLACED_DCT') - low_delay = flags.flag_property('LOW_DELAY') - global_header = flags.flag_property('GLOBAL_HEADER') - bitexact = flags.flag_property('BITEXACT') - ac_pred = flags.flag_property('AC_PRED') - interlaced_me = flags.flag_property('INTERLACED_ME') - closed_gop = flags.flag_property('CLOSED_GOP') + unaligned = flags.flag_property("UNALIGNED") + qscale = flags.flag_property("QSCALE") + four_mv = flags.flag_property("4MV") + output_corrupt = flags.flag_property("OUTPUT_CORRUPT") + qpel = flags.flag_property("QPEL") + drop_changed = flags.flag_property("DROPCHANGED") + pass1 = flags.flag_property("PASS1") + pass2 = flags.flag_property("PASS2") + loop_filter = flags.flag_property("LOOP_FILTER") + gray = flags.flag_property("GRAY") + psnr = flags.flag_property("PSNR") + interlaced_dct = flags.flag_property("INTERLACED_DCT") + low_delay = flags.flag_property("LOW_DELAY") + global_header = flags.flag_property("GLOBAL_HEADER") + bitexact = flags.flag_property("BITEXACT") + ac_pred = flags.flag_property("AC_PRED") + interlaced_me = flags.flag_property("INTERLACED_ME") + closed_gop = flags.flag_property("CLOSED_GOP") def _get_flags2(self): return self.ptr.flags2 @@ -206,60 +205,60 @@ cdef class CodecContext: """Flag property of :class:`.Flags2`.""" ) - fast = flags2.flag_property('FAST') - no_output = flags2.flag_property('NO_OUTPUT') - local_header = flags2.flag_property('LOCAL_HEADER') - chunks = flags2.flag_property('CHUNKS') - ignore_crop = flags2.flag_property('IGNORE_CROP') - show_all = flags2.flag_property('SHOW_ALL') - export_mvs = flags2.flag_property('EXPORT_MVS') - skip_manual = flags2.flag_property('SKIP_MANUAL') - ro_flush_noop = flags2.flag_property('RO_FLUSH_NOOP') - - property extradata: - def __get__(self): - if self.ptr.extradata_size > 0: - return (self.ptr.extradata)[:self.ptr.extradata_size] - else: - return None - - def __set__(self, data): - if not self.is_decoder: - raise ValueError("Can only set extradata for decoders.") - - if data is None: - lib.av_freep(&self.ptr.extradata) - self.ptr.extradata_size = 0 - else: - source = bytesource(data) - self.ptr.extradata = lib.av_realloc(self.ptr.extradata, source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE) - if not self.ptr.extradata: - raise MemoryError("Cannot allocate extradata") - memcpy(self.ptr.extradata, source.ptr, source.length) - self.ptr.extradata_size = source.length - self.extradata_set = True - - property extradata_size: - def __get__(self): - return self.ptr.extradata_size - - property is_open: - def __get__(self): - return lib.avcodec_is_open(self.ptr) - - property is_encoder: - def __get__(self): - return lib.av_codec_is_encoder(self.ptr.codec) - - property is_decoder: - def __get__(self): - return lib.av_codec_is_decoder(self.ptr.codec) + fast = flags2.flag_property("FAST") + no_output = flags2.flag_property("NO_OUTPUT") + local_header = flags2.flag_property("LOCAL_HEADER") + chunks = flags2.flag_property("CHUNKS") + ignore_crop = flags2.flag_property("IGNORE_CROP") + show_all = flags2.flag_property("SHOW_ALL") + export_mvs = flags2.flag_property("EXPORT_MVS") + skip_manual = flags2.flag_property("SKIP_MANUAL") + ro_flush_noop = flags2.flag_property("RO_FLUSH_NOOP") + + @property + def extradata(self): + if self.ptr.extradata_size > 0: + return (self.ptr.extradata)[:self.ptr.extradata_size] + else: + return None + + @extradata.setter + def extradata(self, data): + if not self.is_decoder: + raise ValueError("Can only set extradata for decoders.") + + if data is None: + lib.av_freep(&self.ptr.extradata) + self.ptr.extradata_size = 0 + else: + source = bytesource(data) + self.ptr.extradata = lib.av_realloc(self.ptr.extradata, source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE) + if not self.ptr.extradata: + raise MemoryError("Cannot allocate extradata") + memcpy(self.ptr.extradata, source.ptr, source.length) + self.ptr.extradata_size = source.length + self.extradata_set = True + + @property + def extradata_size(self): + return self.ptr.extradata_size + + @property + def is_open(self): + return lib.avcodec_is_open(self.ptr) + + @property + def is_encoder(self): + return lib.av_codec_is_encoder(self.ptr.codec) + + @property + def is_decoder(self): + return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): - if lib.avcodec_is_open(self.ptr): if strict: - raise ValueError('CodecContext is already open.') + raise ValueError("CodecContext is already open.") return # We might pass partial frames. @@ -288,7 +287,7 @@ cdef class CodecContext: cpdef close(self, bint strict=True): if not lib.avcodec_is_open(self.ptr): if strict: - raise ValueError('CodecContext is already closed.') + raise ValueError("CodecContext is already closed.") return err_check(lib.avcodec_close(self.ptr)) @@ -302,12 +301,9 @@ cdef class CodecContext: lib.av_parser_close(self.parser) def __repr__(self): - return '' % ( - self.__class__.__name__, - self.type or '', - self.name or '', - id(self), - ) + _type = self.type or "" + name = self.name or "" + return f"" def parse(self, raw_input=None): """Split up a byte stream into list of :class:`.Packet`. @@ -329,7 +325,7 @@ cdef class CodecContext: if not self.parser: self.parser = lib.av_parser_init(self.codec.ptr.id) if not self.parser: - raise ValueError('No parser for %s' % self.codec.name) + raise ValueError(f"No parser for {self.codec.name}") cdef ByteSource source = bytesource(raw_input, allow_none=True) @@ -344,7 +340,6 @@ cdef class CodecContext: packets = [] while True: - with nogil: consumed = lib.av_parser_parse2( self.parser, @@ -357,7 +352,6 @@ cdef class CodecContext: err_check(consumed) if out_size: - # We copy the data immediately, as we have yet to figure out # the expected lifetime of the buffer we get back. All of the # examples decode it immediately. @@ -388,8 +382,7 @@ cdef class CodecContext: return packets - cdef _send_frame_and_recv(self, Frame frame): - + def _send_frame_and_recv(self, Frame frame): cdef Packet packet cdef int res @@ -397,17 +390,12 @@ cdef class CodecContext: res = lib.avcodec_send_frame(self.ptr, frame.ptr if frame is not None else NULL) err_check(res) - out = [] - while True: + packet = self._recv_packet() + while packet: + yield packet packet = self._recv_packet() - if packet: - out.append(packet) - else: - break - return out cdef _send_packet_and_recv(self, Packet packet): - cdef Frame frame cdef int res @@ -428,10 +416,9 @@ cdef class CodecContext: return [frame] cdef Frame _alloc_next_frame(self): - raise NotImplementedError('Base CodecContext cannot decode.') + raise NotImplementedError("Base CodecContext cannot decode.") cdef _recv_frame(self): - if not self._next_frame: self._next_frame = self._alloc_next_frame() cdef Frame frame = self._next_frame @@ -449,7 +436,6 @@ cdef class CodecContext: return frame cdef _recv_packet(self): - cdef Packet packet = Packet() cdef int res @@ -462,11 +448,9 @@ cdef class CodecContext: if not res: return packet - cpdef encode(self, Frame frame=None): - """Encode a list of :class:`.Packet` from the given :class:`.Frame`.""" - + cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame): if self.ptr.codec_type not in [lib.AVMEDIA_TYPE_VIDEO, lib.AVMEDIA_TYPE_AUDIO]: - raise NotImplementedError('Encoding is only supported for audio and video.') + raise NotImplementedError("Encoding is only supported for audio and video.") self.open(strict=False) @@ -478,13 +462,23 @@ cdef class CodecContext: if frame is not None: frame._rebase_time(self.ptr.time_base) + return frames + + cpdef encode(self, Frame frame=None): + """Encode a list of :class:`.Packet` from the given :class:`.Frame`.""" res = [] - for frame in frames: + for frame in self._prepare_and_time_rebase_frames_for_encode(frame): for packet in self._send_frame_and_recv(frame): self._setup_encoded_packet(packet) res.append(packet) return res + def encode_lazy(self, Frame frame=None): + for frame in self._prepare_and_time_rebase_frames_for_encode(frame): + for packet in self._send_frame_and_recv(frame): + self._setup_encoded_packet(packet) + yield packet + cdef _setup_encoded_packet(self, Packet packet): # We coerced the frame's time_base into the CodecContext's during encoding, # and FFmpeg copied the frame's pts/dts to the packet, so keep track of @@ -505,7 +499,7 @@ cdef class CodecContext: """ if not self.codec.ptr: - raise ValueError('cannot decode unknown codec') + raise ValueError("cannot decode unknown codec") self.open(strict=False) @@ -523,116 +517,130 @@ cdef class CodecContext: # is carrying around. # TODO: Somehow get this from the stream so we can not pass the # packet here (because flushing packets are bogus). - frame._time_base = packet._time_base + if packet is not None: + frame._time_base = packet._time_base frame.index = self.ptr.frame_number - 1 - property name: - def __get__(self): - return self.codec.name - - property type: - def __get__(self): - return self.codec.type - - property profile: - def __get__(self): - if self.ptr.codec and lib.av_get_profile_name(self.ptr.codec, self.ptr.profile): - return lib.av_get_profile_name(self.ptr.codec, self.ptr.profile) - - property time_base: - def __get__(self): - if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) - return avrational_to_fraction(&self.ptr.time_base) - - def __set__(self, value): - if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) - to_avrational(value, &self.ptr.time_base) - - property codec_tag: - def __get__(self): - return self.ptr.codec_tag.to_bytes(4, byteorder="little", signed=False).decode( - encoding="ascii") - - def __set__(self, value): - if isinstance(value, str) and len(value) == 4: - self.ptr.codec_tag = int.from_bytes(value.encode(encoding="ascii"), - byteorder="little", signed=False) - else: - raise ValueError("Codec tag should be a 4 character string.") - - property ticks_per_frame: - def __get__(self): - return self.ptr.ticks_per_frame - - property bit_rate: - def __get__(self): - return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None - - def __set__(self, int value): - self.ptr.bit_rate = value - - property max_bit_rate: - def __get__(self): - if self.ptr.rc_max_rate > 0: - return self.ptr.rc_max_rate - else: - return None - - property bit_rate_tolerance: - def __get__(self): - self.ptr.bit_rate_tolerance - - def __set__(self, int value): - self.ptr.bit_rate_tolerance = value - - property thread_count: + @property + def name(self): + return self.codec.name + + @property + def type(self): + return self.codec.type + + @property + def profile(self): + if self.ptr.codec and lib.av_get_profile_name(self.ptr.codec, self.ptr.profile): + return lib.av_get_profile_name(self.ptr.codec, self.ptr.profile) + + @property + def time_base(self): + if self.is_decoder: + warnings.warn( + "Using CodecContext.time_base for decoders is deprecated.", + AVDeprecationWarning + ) + return avrational_to_fraction(&self.ptr.time_base) + + @time_base.setter + def time_base(self, value): + if self.is_decoder: + warnings.warn( + "Using CodecContext.time_base for decoders is deprecated.", + AVDeprecationWarning + ) + to_avrational(value, &self.ptr.time_base) + + @property + def codec_tag(self): + return self.ptr.codec_tag.to_bytes(4, byteorder="little", signed=False).decode( + encoding="ascii") + + @codec_tag.setter + def codec_tag(self, value): + if isinstance(value, str) and len(value) == 4: + self.ptr.codec_tag = int.from_bytes(value.encode(encoding="ascii"), + byteorder="little", signed=False) + else: + raise ValueError("Codec tag should be a 4 character string.") + + @property + def ticks_per_frame(self): + return self.ptr.ticks_per_frame + + @property + def bit_rate(self): + return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None + + @bit_rate.setter + def bit_rate(self, int value): + self.ptr.bit_rate = value + + @property + def max_bit_rate(self): + if self.ptr.rc_max_rate > 0: + return self.ptr.rc_max_rate + else: + return None + + @property + def bit_rate_tolerance(self): + self.ptr.bit_rate_tolerance + + @bit_rate_tolerance.setter + def bit_rate_tolerance(self, int value): + self.ptr.bit_rate_tolerance = value + + @property + def thread_count(self): """How many threads to use; 0 means auto. Wraps :ffmpeg:`AVCodecContext.thread_count`. """ + return self.ptr.thread_count - def __get__(self): - return self.ptr.thread_count - - def __set__(self, int value): - if lib.avcodec_is_open(self.ptr): - raise RuntimeError("Cannot change thread_count after codec is open.") - self.ptr.thread_count = value + @thread_count.setter + def thread_count(self, int value): + if lib.avcodec_is_open(self.ptr): + raise RuntimeError("Cannot change thread_count after codec is open.") + self.ptr.thread_count = value - property thread_type: + @property + def thread_type(self): """One of :class:`.ThreadType`. Wraps :ffmpeg:`AVCodecContext.thread_type`. """ + return ThreadType.get(self.ptr.thread_type, create=True) - def __get__(self): - return ThreadType.get(self.ptr.thread_type, create=True) - - def __set__(self, value): - if lib.avcodec_is_open(self.ptr): - raise RuntimeError("Cannot change thread_type after codec is open.") - self.ptr.thread_type = ThreadType[value].value + @thread_type.setter + def thread_type(self, value): + if lib.avcodec_is_open(self.ptr): + raise RuntimeError("Cannot change thread_type after codec is open.") + self.ptr.thread_type = ThreadType[value].value - property skip_frame: + @property + def skip_frame(self): """One of :class:`.SkipType`. Wraps ffmpeg:`AVCodecContext.skip_frame`. """ + return SkipType._get(self.ptr.skip_frame, create=True) - def __get__(self): - return SkipType._get(self.ptr.skip_frame, create=True) + @skip_frame.setter + def skip_frame(self, value): + self.ptr.skip_frame = SkipType[value].value - def __set__(self, value): - self.ptr.skip_frame = SkipType[value].value + @property + def delay(self): + """Codec delay. + + Wraps :ffmpeg:`AVCodecContext.delay`. + + """ + return self.ptr.delay diff --git a/av/container/__init__.pyi b/av/container/__init__.pyi new file mode 100644 index 000000000..7160777cc --- /dev/null +++ b/av/container/__init__.pyi @@ -0,0 +1,3 @@ +from .core import * +from .input import * +from .output import * diff --git a/av/container/core.pxd b/av/container/core.pxd index fb7c3b511..1aed54b90 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -6,7 +6,6 @@ from av.dictionary cimport _Dictionary from av.format cimport ContainerFormat from av.stream cimport Stream - # Interrupt callback information, times are in seconds. ctypedef struct timeout_info: double start_time @@ -37,6 +36,8 @@ cdef class Container: cdef readonly StreamContainer streams cdef readonly dict metadata + # Private API. + cdef _assert_open(self) cdef int err_check(self, int value) except -1 # Timeouts diff --git a/av/container/core.pyi b/av/container/core.pyi new file mode 100644 index 000000000..c33f3a4dc --- /dev/null +++ b/av/container/core.pyi @@ -0,0 +1,114 @@ +from numbers import Real +from pathlib import Path +from types import TracebackType +from typing import Any, Callable, Literal, Type, overload + +from av.enum import EnumFlag + +from .input import InputContainer +from .output import OutputContainer +from .streams import StreamContainer + +class Flags(EnumFlag): + GENPTS: int + IGNIDX: int + NONBLOCK: int + IGNDTS: int + NOFILLIN: int + NOPARSE: int + NOBUFFER: int + CUSTOM_IO: int + DISCARD_CORRUPT: int + FLUSH_PACKETS: int + BITEXACT: int + SORT_DTS: int + FAST_SEEK: int + SHORTEST: int + AUTO_BSF: int + +class Container: + writeable: bool + name: str + metadata_encoding: str + metadata_errors: str + file: Any + buffer_size: int + input_was_opened: bool + io_open: Any + open_files: Any + format: str | None + options: dict[str, str] + container_options: dict[str, str] + stream_options: list[str] + streams: StreamContainer + metadata: dict[str, str] + open_timeout: Real | None + read_timeout: Real | None + + def __enter__(self) -> Container: ... + def __exit__( + self, + exc_type: Type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: ... + def err_check(self, value: int) -> int: ... + def set_timeout(self, timeout: Real | None) -> None: ... + def start_timeout(self) -> None: ... + +@overload +def open( + file: Any, + mode: Literal["r"], + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, +) -> InputContainer: ... +@overload +def open( + file: str | Path, + mode: Literal["r"] | None = None, + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, +) -> InputContainer: ... +@overload +def open( + file: Any, + mode: Literal["w"], + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, +) -> OutputContainer: ... +@overload +def open( + file: Any, + mode: Literal["r", "w"] | None = None, + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, +) -> InputContainer | OutputContainer: ... diff --git a/av/container/core.pyx b/av/container/core.pyx index 5b1711a53..240db340e 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -3,6 +3,7 @@ from libc.stdint cimport int64_t import os import time +from pathlib import Path cimport libav as lib @@ -23,7 +24,7 @@ cdef object _cinit_sentinel = object() # We want to use the monotonic clock if it is available. -cdef object clock = getattr(time, 'monotonic', time.time) +cdef object clock = getattr(time, "monotonic", time.time) cdef int interrupt_cb (void *p) noexcept nogil: @@ -123,41 +124,41 @@ cdef void pyav_io_close_gil(lib.AVFormatContext *s, stash_exception() -Flags = define_enum('Flags', __name__, ( - ('GENPTS', lib.AVFMT_FLAG_GENPTS, +Flags = define_enum("Flags", __name__, ( + ("GENPTS", lib.AVFMT_FLAG_GENPTS, "Generate missing pts even if it requires parsing future frames."), - ('IGNIDX', lib.AVFMT_FLAG_IGNIDX, + ("IGNIDX", lib.AVFMT_FLAG_IGNIDX, "Ignore index."), - ('NONBLOCK', lib.AVFMT_FLAG_NONBLOCK, + ("NONBLOCK", lib.AVFMT_FLAG_NONBLOCK, "Do not block when reading packets from input."), - ('IGNDTS', lib.AVFMT_FLAG_IGNDTS, + ("IGNDTS", lib.AVFMT_FLAG_IGNDTS, "Ignore DTS on frames that contain both DTS & PTS."), - ('NOFILLIN', lib.AVFMT_FLAG_NOFILLIN, + ("NOFILLIN", lib.AVFMT_FLAG_NOFILLIN, "Do not infer any values from other values, just return what is stored in the container."), - ('NOPARSE', lib.AVFMT_FLAG_NOPARSE, + ("NOPARSE", lib.AVFMT_FLAG_NOPARSE, """Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled."""), - ('NOBUFFER', lib.AVFMT_FLAG_NOBUFFER, + ("NOBUFFER", lib.AVFMT_FLAG_NOBUFFER, "Do not buffer frames when possible."), - ('CUSTOM_IO', lib.AVFMT_FLAG_CUSTOM_IO, + ("CUSTOM_IO", lib.AVFMT_FLAG_CUSTOM_IO, "The caller has supplied a custom AVIOContext, don't avio_close() it."), - ('DISCARD_CORRUPT', lib.AVFMT_FLAG_DISCARD_CORRUPT, + ("DISCARD_CORRUPT", lib.AVFMT_FLAG_DISCARD_CORRUPT, "Discard frames marked corrupted."), - ('FLUSH_PACKETS', lib.AVFMT_FLAG_FLUSH_PACKETS, + ("FLUSH_PACKETS", lib.AVFMT_FLAG_FLUSH_PACKETS, "Flush the AVIOContext every packet."), - ('BITEXACT', lib.AVFMT_FLAG_BITEXACT, + ("BITEXACT", lib.AVFMT_FLAG_BITEXACT, """When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing."""), - ('SORT_DTS', lib.AVFMT_FLAG_SORT_DTS, + ("SORT_DTS", lib.AVFMT_FLAG_SORT_DTS, "Try to interleave outputted packets by dts (using this flag can slow demuxing down)."), - ('FAST_SEEK', lib.AVFMT_FLAG_FAST_SEEK, + ("FAST_SEEK", lib.AVFMT_FLAG_FAST_SEEK, "Enable fast, but inaccurate seeks for some formats."), - ('SHORTEST', lib.AVFMT_FLAG_SHORTEST, + ("SHORTEST", lib.AVFMT_FLAG_SHORTEST, "Stop muxing when the shortest stream stops."), - ('AUTO_BSF', lib.AVFMT_FLAG_AUTO_BSF, + ("AUTO_BSF", lib.AVFMT_FLAG_AUTO_BSF, "Add bitstream filters as requested by the muxer."), ), is_flags=True) @@ -171,16 +172,16 @@ cdef class Container: io_open): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot construct base Container') + raise RuntimeError("cannot construct base Container") self.writeable = isinstance(self, OutputContainer) if not self.writeable and not isinstance(self, InputContainer): - raise RuntimeError('Container cannot be directly extended.') + raise RuntimeError("Container cannot be directly extended.") if isinstance(file_, str): self.name = file_ else: - self.name = str(getattr(file_, 'name', '')) + self.name = str(getattr(file_, "name", "")) self.options = dict(options or ()) self.container_options = dict(container_options or ()) @@ -279,15 +280,16 @@ cdef class Container: self.close() def __repr__(self): - return '' % (self.__class__.__name__, self.file or self.name) + return f"" cdef int err_check(self, int value) except -1: return err_check(value, filename=self.name) def dumps_format(self): + self._assert_open() with LogCapture() as logs: lib.av_dump_format(self.ptr, 0, "", isinstance(self, OutputContainer)) - return ''.join(log[2] for log in logs) + return "".join(log[2] for log in logs) cdef set_timeout(self, timeout): if timeout is None: @@ -298,10 +300,16 @@ cdef class Container: cdef start_timeout(self): self.interrupt_callback_info.start_time = clock() + cdef _assert_open(self): + if self.ptr == NULL: + raise AssertionError("Container is not open") + def _get_flags(self): + self._assert_open() return self.ptr.flags def _set_flags(self, value): + self._assert_open() self.ptr.flags = value flags = Flags.property( @@ -310,27 +318,36 @@ cdef class Container: """Flags property of :class:`.Flags`""" ) - gen_pts = flags.flag_property('GENPTS') - ign_idx = flags.flag_property('IGNIDX') - non_block = flags.flag_property('NONBLOCK') - ign_dts = flags.flag_property('IGNDTS') - no_fill_in = flags.flag_property('NOFILLIN') - no_parse = flags.flag_property('NOPARSE') - no_buffer = flags.flag_property('NOBUFFER') - custom_io = flags.flag_property('CUSTOM_IO') - discard_corrupt = flags.flag_property('DISCARD_CORRUPT') - flush_packets = flags.flag_property('FLUSH_PACKETS') - bit_exact = flags.flag_property('BITEXACT') - sort_dts = flags.flag_property('SORT_DTS') - fast_seek = flags.flag_property('FAST_SEEK') - shortest = flags.flag_property('SHORTEST') - auto_bsf = flags.flag_property('AUTO_BSF') - - -def open(file, mode=None, format=None, options=None, - container_options=None, stream_options=None, - metadata_encoding='utf-8', metadata_errors='strict', - buffer_size=32768, timeout=None, io_open=None): + gen_pts = flags.flag_property("GENPTS") + ign_idx = flags.flag_property("IGNIDX") + non_block = flags.flag_property("NONBLOCK") + ign_dts = flags.flag_property("IGNDTS") + no_fill_in = flags.flag_property("NOFILLIN") + no_parse = flags.flag_property("NOPARSE") + no_buffer = flags.flag_property("NOBUFFER") + custom_io = flags.flag_property("CUSTOM_IO") + discard_corrupt = flags.flag_property("DISCARD_CORRUPT") + flush_packets = flags.flag_property("FLUSH_PACKETS") + bit_exact = flags.flag_property("BITEXACT") + sort_dts = flags.flag_property("SORT_DTS") + fast_seek = flags.flag_property("FAST_SEEK") + shortest = flags.flag_property("SHORTEST") + auto_bsf = flags.flag_property("AUTO_BSF") + + +def open( + file, + mode=None, + format=None, + options=None, + container_options=None, + stream_options=None, + metadata_encoding="utf-8", + metadata_errors="strict", + buffer_size=32768, + timeout=None, + io_open=None, +): """open(file, mode='r', **kwargs) Main entrypoint to opening files/streams. @@ -378,34 +395,39 @@ def open(file, mode=None, format=None, options=None, `FFmpeg website `_. """ + if not (mode is None or (isinstance(mode, str) and mode == "r" or mode == "w")): + raise ValueError(f"mode must be 'r', 'w', or None, got: {mode}") + + if isinstance(file, str): + pass + elif isinstance(file, Path): + file = f"{file}" + elif mode is None: + mode = getattr(file, "mode", None) + if mode is None: - mode = getattr(file, 'mode', None) - if mode is None: - mode = 'r' + mode = "r" if isinstance(timeout, tuple): - open_timeout = timeout[0] - read_timeout = timeout[1] + if not len(timeout) == 2: + raise ValueError("timeout must be `float` or `tuple[float, float]`") + + open_timeout, read_timeout = timeout else: open_timeout = timeout read_timeout = timeout - if mode.startswith('r'): - return InputContainer( - _cinit_sentinel, file, format, options, - container_options, stream_options, - metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout, - io_open + if mode.startswith("r"): + return InputContainer(_cinit_sentinel, file, format, options, + container_options, stream_options, metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, io_open, ) - if mode.startswith('w'): - if stream_options: - raise ValueError("Provide stream options via Container.add_stream(..., options={}).") - return OutputContainer( - _cinit_sentinel, file, format, options, - container_options, stream_options, - metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout, - io_open + + if stream_options: + raise ValueError( + "Provide stream options via Container.add_stream(..., options={})." ) - raise ValueError("mode must be 'r' or 'w'; got %r" % mode) + return OutputContainer(_cinit_sentinel, file, format, options, + container_options, stream_options, metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, io_open, + ) diff --git a/av/container/input.pyi b/av/container/input.pyi new file mode 100644 index 000000000..2c1328943 --- /dev/null +++ b/av/container/input.pyi @@ -0,0 +1,44 @@ +from typing import Any, Iterator, Literal, overload + +from av.audio.frame import AudioFrame +from av.audio.stream import AudioStream +from av.packet import Packet +from av.stream import Stream +from av.subtitles.stream import SubtitleStream +from av.subtitles.subtitle import SubtitleSet +from av.video.frame import VideoFrame +from av.video.stream import VideoStream + +from .core import Container + +class InputContainer(Container): + start_time: int + duration: int | None + bit_rate: int + size: int + + def __enter__(self) -> InputContainer: ... + def close(self) -> None: ... + def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]: ... + @overload + def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ... + @overload + def decode(self, *args: AudioStream) -> Iterator[AudioFrame]: ... + @overload + def decode(self, *args: SubtitleStream) -> Iterator[SubtitleSet]: ... + @overload + def decode( + self, *args: Any, **kwargs: Any + ) -> Iterator[VideoFrame | AudioFrame | SubtitleSet]: ... + def seek( + self, + offset: int, + *, + whence: Literal["time"] = "time", + backward: bool = True, + any_frame: bool = False, + stream: Stream | VideoStream | AudioStream | None = None, + unsupported_frame_offset: bool = False, + unsupported_byte_offset: bool = False, + ) -> None: ... + def flush_buffers(self) -> None: ... diff --git a/av/container/input.pyx b/av/container/input.pyx index 80cd8f783..47cd98c4d 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -15,14 +15,13 @@ from av.dictionary import Dictionary cdef close_input(InputContainer self): if self.input_was_opened: with nogil: + # This causes `self.ptr` to be set to NULL. lib.avformat_close_input(&self.ptr) self.input_was_opened = False cdef class InputContainer(Container): - def __cinit__(self, *args, **kwargs): - cdef CodecContext py_codec_context cdef unsigned int i cdef lib.AVStream *stream @@ -88,21 +87,27 @@ cdef class InputContainer(Container): def __dealloc__(self): close_input(self) - property start_time: - def __get__(self): - if self.ptr.start_time != lib.AV_NOPTS_VALUE: - return self.ptr.start_time + @property + def start_time(self): + self._assert_open() + if self.ptr.start_time != lib.AV_NOPTS_VALUE: + return self.ptr.start_time - property duration: - def __get__(self): - if self.ptr.duration != lib.AV_NOPTS_VALUE: - return self.ptr.duration + @property + def duration(self): + self._assert_open() + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration - property bit_rate: - def __get__(self): return self.ptr.bit_rate + @property + def bit_rate(self): + self._assert_open() + return self.ptr.bit_rate - property size: - def __get__(self): return lib.avio_size(self.ptr.pb) + @property + def size(self): + self._assert_open() + return lib.avio_size(self.ptr.pb) def close(self): close_input(self) @@ -123,6 +128,7 @@ cdef class InputContainer(Container): .. note:: The last packets are dummy packets that when decoded will flush the buffers. """ + self._assert_open() # For whatever reason, Cython does not like us directly passing kwargs # from one method to another. Without kwargs, it ends up passing a @@ -143,17 +149,15 @@ cdef class InputContainer(Container): self.set_timeout(self.read_timeout) try: - for i in range(self.ptr.nb_streams): include_stream[i] = False for stream in streams: i = stream.index if i >= self.ptr.nb_streams: - raise ValueError('stream index %d out of range' % i) + raise ValueError(f"stream index {i} out of range") include_stream[i] = True while True: - packet = Packet() try: self.start_timeout() @@ -198,12 +202,13 @@ cdef class InputContainer(Container): the arguments. """ + self._assert_open() id(kwargs) # Avoid Cython bug; see demux(). for packet in self.demux(*args, **kwargs): for frame in packet.decode(): yield frame - def seek(self, offset, *, str whence='time', bint backward=True, + def seek(self, offset, *, str whence="time", bint backward=True, bint any_frame=False, Stream stream=None, bint unsupported_frame_offset=False, bint unsupported_byte_offset=False): @@ -234,21 +239,23 @@ cdef class InputContainer(Container): .. seealso:: :ffmpeg:`avformat_seek_file` for discussion of the flags. """ + self._assert_open() # We used to take floats here and assume they were in seconds. This # was super confusing, so lets go in the complete opposite direction # and reject non-ints. - if not isinstance(offset, (int, long)): - raise TypeError('Container.seek only accepts integer offset.', type(offset)) + if not isinstance(offset, int): + raise TypeError("Container.seek only accepts integer offset.", type(offset)) + cdef int64_t c_offset = offset cdef int flags = 0 cdef int ret # We used to support whence in 'time', 'frame', and 'byte', but later - # realized that FFmpged doens't implement the frame or byte ones. + # realized that FFmpeg doens't implement the frame or byte ones. # We don't even document this anymore, but do allow 'time' to pass through. - if whence != 'time': + if whence != "time": raise ValueError("whence != 'time' is no longer supported") if backward: @@ -270,6 +277,8 @@ cdef class InputContainer(Container): self.flush_buffers() cdef flush_buffers(self): + self._assert_open() + cdef Stream stream cdef CodecContext codec_context diff --git a/av/container/output.pyi b/av/container/output.pyi new file mode 100644 index 000000000..0400dadbb --- /dev/null +++ b/av/container/output.pyi @@ -0,0 +1,21 @@ +from fractions import Fraction +from typing import Sequence + +from av.packet import Packet +from av.stream import Stream + +from .core import Container + +class OutputContainer(Container): + def __enter__(self) -> OutputContainer: ... + def add_stream( + self, + codec_name: str | None = None, + rate: Fraction | int | float | None = None, + template: Stream | None = None, + options: dict[str, str] | None = None, + ) -> Stream: ... + def start_encoding(self) -> None: ... + def close(self) -> None: ... + def mux(self, packets: Sequence[Packet]) -> None: ... + def mux_one(self, packet: Packet) -> None: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 788b3214d..55e8b5006 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -12,7 +12,6 @@ from av.utils cimport dict_to_avdict, to_avrational from av.dictionary import Dictionary - log = logging.getLogger(__name__) @@ -30,7 +29,6 @@ cdef close_output(OutputContainer self): cdef class OutputContainer(Container): - def __cinit__(self, *args, **kwargs): self.streams = StreamContainer() self.metadata = {} @@ -60,13 +58,13 @@ cdef class OutputContainer(Container): """ if (codec_name is None and template is None) or (codec_name is not None and template is not None): - raise ValueError('needs one of codec_name or template') + raise ValueError("needs one of codec_name or template") cdef const lib.AVCodec *codec cdef Codec codec_obj if codec_name is not None: - codec_obj = codec_name if isinstance(codec_name, Codec) else Codec(codec_name, 'w') + codec_obj = codec_name if isinstance(codec_name, Codec) else Codec(codec_name, "w") else: if not template.codec_context: raise ValueError("template has no codec context") @@ -74,12 +72,10 @@ cdef class OutputContainer(Container): codec = codec_obj.ptr # Assert that this format supports the requested codec. - if not lib.avformat_query_codec( - self.ptr.oformat, - codec.id, - lib.FF_COMPLIANCE_NORMAL, - ): - raise ValueError("%r format does not support %r codec" % (self.format.name, codec_name)) + if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + raise ValueError( + f"{self.format.name!r} format does not support {codec_name!r} codec" + ) # Create new stream in the AVFormatContext, set AVCodecContext values. lib.avformat_new_stream(self.ptr, codec) @@ -192,7 +188,7 @@ cdef class OutputContainer(Container): # ... and warn if any weren't used. unused_options = {k: v for k, v in self.options.items() if k not in used_options} if unused_options: - log.warning('Some options were not used: %s' % unused_options) + log.warning("Some options were not used: %s" % unused_options) self._started = True @@ -218,7 +214,7 @@ cdef class OutputContainer(Container): # Assert the packet is in stream time. if packet.ptr.stream_index < 0 or packet.ptr.stream_index >= self.ptr.nb_streams: - raise ValueError('Bad Packet stream_index.') + raise ValueError("Bad Packet stream_index.") cdef lib.AVStream *stream = self.ptr.streams[packet.ptr.stream_index] packet._rebase_time(stream.time_base) diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index e93a11dc8..b3ec04087 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport int64_t, uint8_t cimport libav as lib +from libc.stdint cimport int64_t, uint8_t cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index ed79b44f8..ab29cee11 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -1,28 +1,25 @@ -from libc.string cimport memcpy cimport libav as lib +from libc.string cimport memcpy from av.error cimport stash_exception - ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil cdef class PyIOFile: - def __cinit__(self, file, buffer_size, writeable=None): - self.file = file cdef seek_func_t seek_func = NULL - readable = getattr(self.file, 'readable', None) - writable = getattr(self.file, 'writable', None) - seekable = getattr(self.file, 'seekable', None) - self.fread = getattr(self.file, 'read', None) - self.fwrite = getattr(self.file, 'write', None) - self.fseek = getattr(self.file, 'seek', None) - self.ftell = getattr(self.file, 'tell', None) - self.fclose = getattr(self.file, 'close', None) + readable = getattr(self.file, "readable", None) + writable = getattr(self.file, "writable", None) + seekable = getattr(self.file, "seekable", None) + self.fread = getattr(self.file, "read", None) + self.fwrite = getattr(self.file, "write", None) + self.fseek = getattr(self.file, "seek", None) + self.ftell = getattr(self.file, "tell", None) + self.fclose = getattr(self.file, "close", None) # To be seekable the file object must have `seek` and `tell` methods. # If it also has a `seekable` method, it must return True. diff --git a/av/container/streams.pyi b/av/container/streams.pyi new file mode 100644 index 000000000..cb658d35f --- /dev/null +++ b/av/container/streams.pyi @@ -0,0 +1,30 @@ +from typing import Iterator, overload + +from av.audio.stream import AudioStream +from av.data.stream import DataStream +from av.stream import Stream +from av.subtitles.stream import SubtitleStream +from av.video.stream import VideoStream + +class StreamContainer: + video: tuple[VideoStream, ...] + audio: tuple[AudioStream, ...] + subtitles: tuple[SubtitleStream, ...] + data: tuple[DataStream, ...] + other: tuple[Stream, ...] + + def __init__(self) -> None: ... + def add_stream(self, stream: Stream) -> None: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[Stream]: ... + @overload + def __getitem__(self, index: int) -> Stream: ... + @overload + def __getitem__(self, index: slice) -> list[Stream]: ... + @overload + def __getitem__(self, index: int | slice) -> Stream | list[Stream]: ... + def get( + self, + *args: int | Stream | dict[str, int | tuple[int, ...]], + **kwargs: int | tuple[int, ...], + ) -> list[Stream]: ... diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 8de28a13e..921bd40b2 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -95,7 +95,6 @@ cdef class StreamContainer: selection = [] for x in _flatten((args, kwargs)): - if x is None: pass @@ -107,7 +106,7 @@ cdef class StreamContainer: elif isinstance(x, dict): for type_, indices in x.items(): - if type_ == 'streams': # For compatibility with the pseudo signature + if type_ == "streams": # For compatibility with the pseudo signature streams = self._streams else: streams = getattr(self, type_) @@ -117,6 +116,6 @@ cdef class StreamContainer: selection.append(streams[i]) else: - raise TypeError('Argument must be Stream or int.', type(x)) + raise TypeError("Argument must be Stream or int.", type(x)) return selection or self._streams[:] diff --git a/av/data/stream.pyi b/av/data/stream.pyi new file mode 100644 index 000000000..f84e5f9c2 --- /dev/null +++ b/av/data/stream.pyi @@ -0,0 +1,7 @@ +from av.frame import Frame +from av.packet import Packet +from av.stream import Stream + +class DataStream(Stream): + def encode(self, frame: Frame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None, count: int = 0) -> list[Frame]: ... diff --git a/av/data/stream.pyx b/av/data/stream.pyx index c019961d0..8136c6395 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -2,14 +2,10 @@ cimport libav as lib cdef class DataStream(Stream): - def __repr__(self): - return '' % ( - self.__class__.__name__, - self.index, - self.type or '', - self.name or '', - id(self), + return ( + f"'}/" + f"{self.name or ''} at 0x{id(self):x}>" ) def encode(self, frame=None): @@ -18,9 +14,9 @@ cdef class DataStream(Stream): def decode(self, packet=None, count=0): return [] - property name: - def __get__(self): - cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) - if desc == NULL: - return None - return desc.name + @property + def name(self): + cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) + if desc == NULL: + return None + return desc.name diff --git a/av/datasets.py b/av/datasets.py index b3a5eed85..15ffe9643 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -1,9 +1,8 @@ -from urllib.request import urlopen import errno import logging import os import sys - +from urllib.request import urlopen log = logging.getLogger(__name__) @@ -60,7 +59,7 @@ def cached_download(url, name): clean_name = os.path.normpath(name) if clean_name != name: - raise ValueError("{} is not normalized.".format(name)) + raise ValueError(f"{name} is not normalized.") for dir_ in iter_data_dirs(): path = os.path.join(dir_, name) @@ -70,11 +69,11 @@ def cached_download(url, name): dir_ = next(iter_data_dirs(True)) path = os.path.join(dir_, name) - log.info("Downloading {} to {}".format(url, path)) + log.info(f"Downloading {url} to {path}") response = urlopen(url) if response.getcode() != 200: - raise ValueError("HTTP {}".format(response.getcode())) + raise ValueError(f"HTTP {response.getcode()}") dir_ = os.path.dirname(path) try: diff --git a/av/deprecation.py b/av/deprecation.py index f36d2fe6f..b9a65f135 100644 --- a/av/deprecation.py +++ b/av/deprecation.py @@ -22,7 +22,6 @@ class MethodDeprecationWarning(AVDeprecationWarning): class renamed_attr: - """Proxy for renamed attributes (or methods) on classes. Getting and setting values will be redirected to the provided name, and warnings will be issues every time. @@ -75,7 +74,7 @@ def __init__(self, func): def __get__(self, instance, cls): warning = MethodDeprecationWarning( - "{}.{} is deprecated.".format(cls.__name__, self.func.__name__) + f"{cls.__name__}.{self.func.__name__} is deprecated." ) warnings.warn(warning, stacklevel=2) return self.func.__get__(instance, cls) diff --git a/av/descriptor.pyi b/av/descriptor.pyi new file mode 100644 index 000000000..ae1998391 --- /dev/null +++ b/av/descriptor.pyi @@ -0,0 +1,7 @@ +from typing import NoReturn + +from .option import Option + +class Descriptor: + name: str + options: tuple[Option, ...] diff --git a/av/descriptor.pyx b/av/descriptor.pyx index 8debfa35e..720db1625 100644 --- a/av/descriptor.pyx +++ b/av/descriptor.pyx @@ -14,46 +14,46 @@ cdef Descriptor wrap_avclass(const lib.AVClass *ptr): cdef class Descriptor: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('Cannot construct av.Descriptor') - - property name: - def __get__(self): return self.ptr.class_name if self.ptr.class_name else None - - property options: - def __get__(self): - cdef const lib.AVOption *ptr = self.ptr.option - cdef const lib.AVOption *choice_ptr - cdef Option option - cdef OptionChoice option_choice - cdef bint choice_is_default - if self._options is None: - options = [] - ptr = self.ptr.option - while ptr != NULL and ptr.name != NULL: - if ptr.type == lib.AV_OPT_TYPE_CONST: - ptr += 1 - continue - choices = [] - if ptr.unit != NULL: # option has choices (matching const options) - choice_ptr = self.ptr.option - while choice_ptr != NULL and choice_ptr.name != NULL: - if choice_ptr.type != lib.AV_OPT_TYPE_CONST or choice_ptr.unit != ptr.unit: - choice_ptr += 1 - continue - choice_is_default = (choice_ptr.default_val.i64 == ptr.default_val.i64 or - ptr.type == lib.AV_OPT_TYPE_FLAGS and - choice_ptr.default_val.i64 & ptr.default_val.i64) - option_choice = wrap_option_choice(choice_ptr, choice_is_default) - choices.append(option_choice) - choice_ptr += 1 - option = wrap_option(tuple(choices), ptr) - options.append(option) + raise RuntimeError("Cannot construct av.Descriptor") + + @property + def name(self): + return self.ptr.class_name if self.ptr.class_name else None + + @property + def options(self): + cdef const lib.AVOption *ptr = self.ptr.option + cdef const lib.AVOption *choice_ptr + cdef Option option + cdef OptionChoice option_choice + cdef bint choice_is_default + if self._options is None: + options = [] + ptr = self.ptr.option + while ptr != NULL and ptr.name != NULL: + if ptr.type == lib.AV_OPT_TYPE_CONST: ptr += 1 - self._options = tuple(options) - return self._options + continue + choices = [] + if ptr.unit != NULL: # option has choices (matching const options) + choice_ptr = self.ptr.option + while choice_ptr != NULL and choice_ptr.name != NULL: + if choice_ptr.type != lib.AV_OPT_TYPE_CONST or choice_ptr.unit != ptr.unit: + choice_ptr += 1 + continue + choice_is_default = (choice_ptr.default_val.i64 == ptr.default_val.i64 or + ptr.type == lib.AV_OPT_TYPE_FLAGS and + choice_ptr.default_val.i64 & ptr.default_val.i64) + option_choice = wrap_option_choice(choice_ptr, choice_is_default) + choices.append(option_choice) + choice_ptr += 1 + option = wrap_option(tuple(choices), ptr) + options.append(option) + ptr += 1 + self._options = tuple(options) + return self._options def __repr__(self): - return '<%s %s at 0x%x>' % (self.__class__.__name__, self.name, id(self)) + return f"<{self.__class__.__name__} {self.name} at 0x{id(self):x}>" diff --git a/av/dictionary.pyi b/av/dictionary.pyi new file mode 100644 index 000000000..a6868bea2 --- /dev/null +++ b/av/dictionary.pyi @@ -0,0 +1,10 @@ +from collections.abc import MutableMapping +from typing import Iterator + +class Dictionary(MutableMapping[str, str]): + def __getitem__(self, key: str) -> str: ... + def __setitem__(self, key: str, value: str) -> None: ... + def __delitem__(self, key: str) -> None: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[str]: ... + def __repr__(self) -> str: ... diff --git a/av/dictionary.pyx b/av/dictionary.pyx index 3ebc09b89..15de38381 100644 --- a/av/dictionary.pyx +++ b/av/dictionary.pyx @@ -4,7 +4,6 @@ from av.error cimport err_check cdef class _Dictionary: - def __cinit__(self, *args, **kwargs): for arg in args: self.update(arg) @@ -40,7 +39,7 @@ cdef class _Dictionary: yield element.key def __repr__(self): - return 'av.Dictionary(%r)' % dict(self) + return f"av.Dictionary({dict(self)!r})" cpdef _Dictionary copy(self): cdef _Dictionary other = Dictionary() diff --git a/av/enum.pyi b/av/enum.pyi new file mode 100644 index 000000000..a4fe92e51 --- /dev/null +++ b/av/enum.pyi @@ -0,0 +1,67 @@ +from typing import Any, Callable, Iterable, Literal, Sequence, overload + +class EnumType(type): + def __init__( + self, + name: str, + bases: tuple[type, ...], + attrs: dict[str, Any], + items: Iterable[tuple[str, Any, str | None, bool]], + ) -> None: ... + def _create( + self, name: str, value: int, doc: str | None = None, by_value_only: bool = False + ) -> None: ... + def __len__(self) -> None: ... + def __iter__(self) -> None: ... + def __getitem__(self, key: str | int | EnumType) -> None: ... + def _get(self, value: int, create: bool = False) -> None: ... + def _get_multi_flags(self, value: int) -> None: ... + def get( + self, + key: str | int | EnumType, + default: int | None = None, + create: bool = False, + ) -> int | None: ... + +class EnumItem: + name: str + value: int + + def __int__(self) -> int: ... + def __hash__(self) -> int: ... + def __reduce__( + self, + ) -> tuple[Callable[[str, str, str], EnumItem], tuple[str, str, str]]: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + +class EnumFlag(EnumItem): + flags: tuple[EnumFlag] + + def __and__(self, other: object) -> EnumFlag: ... + def __or__(self, other: object) -> EnumFlag: ... + def __xor__(self, other: object) -> EnumFlag: ... + def __invert__(self) -> bool: ... + def __nonzero__(self) -> bool: ... + +@overload +def define_enum( + name: str, + module: str, + items: Sequence[tuple[str, int] | None], + is_flags: Literal[True], +) -> EnumFlag: ... +@overload +def define_enum( + name: str, + module: str, + items: Sequence[tuple[str, int] | None], + is_flags: Literal[False], +) -> EnumItem: ... +@overload +def define_enum( + name: str, + module: str, + items: Sequence[tuple[str, int] | None], + is_flags: bool = False, +) -> EnumItem | EnumFlag: ... diff --git a/av/enum.pyx b/av/enum.pyx index 522948bbb..a21d66e81 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -16,13 +16,11 @@ cdef sentinel = object() class EnumType(type): - def __new__(mcl, name, bases, attrs, *args): # Just adapting the method signature. return super().__new__(mcl, name, bases, attrs) def __init__(self, name, bases, attrs, items): - self._by_name = {} self._by_value = {} self._all = [] @@ -31,7 +29,6 @@ class EnumType(type): self._create(*spec) def _create(self, name, value, doc=None, by_value_only=False): - # We only have one instance per value. try: item = self._by_value[value] @@ -53,7 +50,6 @@ class EnumType(type): return iter(self._all) def __getitem__(self, key): - if isinstance(key, str): return self._by_name[key] @@ -71,12 +67,9 @@ class EnumType(type): if isinstance(key, self): return key - raise TypeError("{0} indices must be str, int, or {0}".format( - self.__name__, - )) + raise TypeError(f"{self.__name__} indices must be str, int, or itself") def _get(self, long value, bint create=False): - try: return self._by_value[value] except KeyError: @@ -85,10 +78,9 @@ class EnumType(type): if not create: return - return self._create('{}_{}'.format(self.__name__.upper(), value), value, by_value_only=True) + return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) def _get_multi_flags(self, long value): - try: return self._by_value[value] except KeyError: @@ -106,7 +98,7 @@ class EnumType(type): if to_find: raise KeyError(value) - name = '|'.join(f.name for f in flags) + name = "|".join(f.name for f in flags) cdef EnumFlag combo = self._create(name, value, by_value_only=True) combo.flags = tuple(flags) @@ -125,7 +117,7 @@ class EnumType(type): def _unpickle(mod_name, cls_name, item_name): - mod = __import__(mod_name, fromlist=['.']) + mod = __import__(mod_name, fromlist=["."]) cls = getattr(mod, cls_name) return cls[item_name] @@ -134,7 +126,6 @@ copyreg.constructor(_unpickle) cdef class EnumItem: - """ Enumerations are when an attribute may only take on a single value at once, and they are represented as integers in the FFmpeg API. We associate names with each @@ -171,9 +162,8 @@ cdef class EnumItem: cdef Py_hash_t _hash def __cinit__(self, sentinel_, str name, int value, doc=None): - if sentinel_ is not sentinel: - raise RuntimeError("Cannot instantiate {}.".format(self.__class__.__name__)) + raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") self.name = name self.value = value @@ -190,12 +180,7 @@ cdef class EnumItem: self._hash = hash_ def __repr__(self): - return '<{}.{}:{}(0x{:x})>'.format( - self.__class__.__module__, - self.__class__.__name__, - self.name, - self.value, - ) + return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>" def __str__(self): return self.name @@ -210,19 +195,16 @@ cdef class EnumItem: return (_unpickle, (self.__class__.__module__, self.__class__.__name__, self.name)) def __eq__(self, other): - if isinstance(other, str): - if self.name == other: # The quick method. return True try: other_inst = self.__class__._by_name[other] except KeyError: - raise ValueError("{} does not have item named {!r}".format( - self.__class__.__name__, - other, - )) + raise ValueError( + f"{self.__class__.__name__} does not have item named {other!r}" + ) else: return self is other_inst @@ -231,18 +213,16 @@ cdef class EnumItem: return True if other in self.__class__._by_value: return False - raise ValueError("{} does not have item valued {}".format( - self.__class__.__name__, - other, - )) + raise ValueError( + f"{self.__class__.__name__} does not have item valued {other}" + ) if isinstance(other, self.__class__): return self is other - raise TypeError("'==' not supported between {} and {}".format( - self.__class__.__name__, - type(other).__name__, - )) + raise TypeError( + f"'==' not supported between {self.__class__.__name__} and {type(other).__name__}" + ) def __ne__(self, other): return not (self == other) @@ -321,7 +301,6 @@ cdef class EnumFlag(EnumItem): cdef class EnumProperty: - cdef object enum cdef object fget cdef object fset @@ -383,6 +362,6 @@ cpdef define_enum(name, module, items, bint is_flags=False): base_cls = EnumItem # Some items may be None if they correspond to an unsupported FFmpeg feature - cls = EnumType(name, (base_cls, ), {'__module__': module}, [i for i in items if i is not None]) + cls = EnumType(name, (base_cls, ), {"__module__": module}, [i for i in items if i is not None]) return cls diff --git a/av/error.pyi b/av/error.pyi new file mode 100644 index 000000000..db6581238 --- /dev/null +++ b/av/error.pyi @@ -0,0 +1,61 @@ +from .enum import EnumItem + +classes: dict[int, Exception] + +def code_to_tag(code: int) -> bytes: ... +def tag_to_code(tag: bytes) -> int: ... +def make_error( + res: int, + filename: str | None = None, + log: tuple[int, tuple[int, str, str] | None] | None = None, +) -> None: ... + +class ErrorType(EnumItem): + BSF_NOT_FOUND: int + BUG: int + BUFFER_TOO_SMALL: int + DECODER_NOT_FOUND: int + DEMUXER_NOT_FOUND: int + ENCODER_NOT_FOUND: int + EOF: int + EXIT: int + EXTERNAL: int + FILTER_NOT_FOUND: int + INVALIDDATA: int + MUXER_NOT_FOUND: int + OPTION_NOT_FOUND: int + PATCHWELCOME: int + PROTOCOL_NOT_FOUND: int + UNKNOWN: int + EXPERIMENTAL: int + INPUT_CHANGED: int + OUTPUT_CHANGED: int + HTTP_BAD_REQUEST: int + HTTP_UNAUTHORIZED: int + HTTP_FORBIDDEN: int + HTTP_NOT_FOUND: int + HTTP_OTHER_4XX: int + HTTP_SERVER_ERROR: int + PYAV_CALLBACK: int + + tag: bytes + +class FFmpegError(Exception): + errno: int + strerror: str + filename: str + log: tuple[int, tuple[int, str, str] | None] + + def __init__( + self, + code: int, + message: str, + filename: str | None = None, + log: tuple[int, tuple[int, str, str] | None] | None = None, + ) -> None: ... + +class LookupError(FFmpegError): ... +class HTTPError(FFmpegError): ... +class HTTPClientError(FFmpegError): ... +class UndefinedError(FFmpegError): ... +class InvalidDataError(ValueError): ... diff --git a/av/error.pyx b/av/error.pyx index cde4fec7f..64cd70594 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -2,19 +2,18 @@ cimport libav as lib from av.logging cimport get_last_error -from threading import local import errno import os import sys import traceback +from threading import local from av.enum import define_enum - # Will get extended with all of the exceptions. __all__ = [ - 'ErrorType', 'FFmpegError', 'LookupError', 'HTTPError', 'HTTPClientError', - 'UndefinedError', + "ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError", + "UndefinedError", ] @@ -50,7 +49,6 @@ cpdef tag_to_code(bytes tag): class FFmpegError(Exception): - """Exception class for errors from within FFmpeg. .. attribute:: errno @@ -108,21 +106,20 @@ class FFmpegError(Exception): pass def __str__(self): - - msg = f'[Errno {self.errno}] {self.strerror}' + msg = f"[Errno {self.errno}] {self.strerror}" if self.filename: - msg = f'{msg}: {self.filename!r}' + msg = f"{msg}: {self.filename!r}" if self.log: - msg = f'{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}' + msg = f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" return msg # Our custom error, used in callbacks. -cdef int c_PYAV_STASHED_ERROR = tag_to_code(b'PyAV') -cdef str PYAV_STASHED_ERROR_message = 'Error in PyAV callback' +cdef int c_PYAV_STASHED_ERROR = tag_to_code(b"PyAV") +cdef str PYAV_STASHED_ERROR_message = "Error in PyAV callback" # Bases for the FFmpeg-based exceptions. @@ -140,32 +137,32 @@ class HTTPClientError(FFmpegError): # Tuples of (enum_name, enum_value, exc_name, exc_base). _ffmpeg_specs = ( - ('BSF_NOT_FOUND', -lib.AVERROR_BSF_NOT_FOUND, 'BSFNotFoundError', LookupError), - ('BUG', -lib.AVERROR_BUG, None, RuntimeError), - ('BUFFER_TOO_SMALL', -lib.AVERROR_BUFFER_TOO_SMALL, None, ValueError), - ('DECODER_NOT_FOUND', -lib.AVERROR_DECODER_NOT_FOUND, None, LookupError), - ('DEMUXER_NOT_FOUND', -lib.AVERROR_DEMUXER_NOT_FOUND, None, LookupError), - ('ENCODER_NOT_FOUND', -lib.AVERROR_ENCODER_NOT_FOUND, None, LookupError), - ('EOF', -lib.AVERROR_EOF, 'EOFError', EOFError), - ('EXIT', -lib.AVERROR_EXIT, None, None), - ('EXTERNAL', -lib.AVERROR_EXTERNAL, None, None), - ('FILTER_NOT_FOUND', -lib.AVERROR_FILTER_NOT_FOUND, None, LookupError), - ('INVALIDDATA', -lib.AVERROR_INVALIDDATA, 'InvalidDataError', ValueError), - ('MUXER_NOT_FOUND', -lib.AVERROR_MUXER_NOT_FOUND, None, LookupError), - ('OPTION_NOT_FOUND', -lib.AVERROR_OPTION_NOT_FOUND, None, LookupError), - ('PATCHWELCOME', -lib.AVERROR_PATCHWELCOME, 'PatchWelcomeError', None), - ('PROTOCOL_NOT_FOUND', -lib.AVERROR_PROTOCOL_NOT_FOUND, None, LookupError), - ('UNKNOWN', -lib.AVERROR_UNKNOWN, None, None), - ('EXPERIMENTAL', -lib.AVERROR_EXPERIMENTAL, None, None), - ('INPUT_CHANGED', -lib.AVERROR_INPUT_CHANGED, None, None), - ('OUTPUT_CHANGED', -lib.AVERROR_OUTPUT_CHANGED, None, None), - ('HTTP_BAD_REQUEST', -lib.AVERROR_HTTP_BAD_REQUEST, 'HTTPBadRequestError', HTTPClientError), - ('HTTP_UNAUTHORIZED', -lib.AVERROR_HTTP_UNAUTHORIZED, 'HTTPUnauthorizedError', HTTPClientError), - ('HTTP_FORBIDDEN', -lib.AVERROR_HTTP_FORBIDDEN, 'HTTPForbiddenError', HTTPClientError), - ('HTTP_NOT_FOUND', -lib.AVERROR_HTTP_NOT_FOUND, 'HTTPNotFoundError', HTTPClientError), - ('HTTP_OTHER_4XX', -lib.AVERROR_HTTP_OTHER_4XX, 'HTTPOtherClientError', HTTPClientError), - ('HTTP_SERVER_ERROR', -lib.AVERROR_HTTP_SERVER_ERROR, 'HTTPServerError', HTTPError), - ('PYAV_CALLBACK', c_PYAV_STASHED_ERROR, 'PyAVCallbackError', RuntimeError), + ("BSF_NOT_FOUND", -lib.AVERROR_BSF_NOT_FOUND, "BSFNotFoundError", LookupError), + ("BUG", -lib.AVERROR_BUG, None, RuntimeError), + ("BUFFER_TOO_SMALL", -lib.AVERROR_BUFFER_TOO_SMALL, None, ValueError), + ("DECODER_NOT_FOUND", -lib.AVERROR_DECODER_NOT_FOUND, None, LookupError), + ("DEMUXER_NOT_FOUND", -lib.AVERROR_DEMUXER_NOT_FOUND, None, LookupError), + ("ENCODER_NOT_FOUND", -lib.AVERROR_ENCODER_NOT_FOUND, None, LookupError), + ("EOF", -lib.AVERROR_EOF, "EOFError", EOFError), + ("EXIT", -lib.AVERROR_EXIT, None, None), + ("EXTERNAL", -lib.AVERROR_EXTERNAL, None, None), + ("FILTER_NOT_FOUND", -lib.AVERROR_FILTER_NOT_FOUND, None, LookupError), + ("INVALIDDATA", -lib.AVERROR_INVALIDDATA, "InvalidDataError", ValueError), + ("MUXER_NOT_FOUND", -lib.AVERROR_MUXER_NOT_FOUND, None, LookupError), + ("OPTION_NOT_FOUND", -lib.AVERROR_OPTION_NOT_FOUND, None, LookupError), + ("PATCHWELCOME", -lib.AVERROR_PATCHWELCOME, "PatchWelcomeError", None), + ("PROTOCOL_NOT_FOUND", -lib.AVERROR_PROTOCOL_NOT_FOUND, None, LookupError), + ("UNKNOWN", -lib.AVERROR_UNKNOWN, None, None), + ("EXPERIMENTAL", -lib.AVERROR_EXPERIMENTAL, None, None), + ("INPUT_CHANGED", -lib.AVERROR_INPUT_CHANGED, None, None), + ("OUTPUT_CHANGED", -lib.AVERROR_OUTPUT_CHANGED, None, None), + ("HTTP_BAD_REQUEST", -lib.AVERROR_HTTP_BAD_REQUEST, "HTTPBadRequestError", HTTPClientError), + ("HTTP_UNAUTHORIZED", -lib.AVERROR_HTTP_UNAUTHORIZED, "HTTPUnauthorizedError", HTTPClientError), + ("HTTP_FORBIDDEN", -lib.AVERROR_HTTP_FORBIDDEN, "HTTPForbiddenError", HTTPClientError), + ("HTTP_NOT_FOUND", -lib.AVERROR_HTTP_NOT_FOUND, "HTTPNotFoundError", HTTPClientError), + ("HTTP_OTHER_4XX", -lib.AVERROR_HTTP_OTHER_4XX, "HTTPOtherClientError", HTTPClientError), + ("HTTP_SERVER_ERROR", -lib.AVERROR_HTTP_SERVER_ERROR, "HTTPServerError", HTTPError), + ("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError), ) @@ -188,7 +185,6 @@ ErrorType.tag = property(lambda self: code_to_tag(self.value)) for enum in ErrorType: - # Mimick the errno module. globals()[enum.name] = enum if enum.value == c_PYAV_STASHED_ERROR: @@ -200,23 +196,20 @@ for enum in ErrorType: # Mimick the builtin exception types. # See https://www.python.org/dev/peps/pep-3151/#new-exception-classes # Use the named ones we have, otherwise default to OSError for anything in errno. -r''' - -See this command for the count of POSIX codes used: - egrep -IR 'AVERROR\(E[A-Z]+\)' vendor/ffmpeg-4.2 |\ - sed -E 's/.*AVERROR\((E[A-Z]+)\).*/\1/' | \ - sort | uniq -c - -The biggest ones that don't map to PEP 3151 builtins: - - 2106 EINVAL -> ValueError - 649 EIO -> IOError (if it is distinct from OSError) - 4080 ENOMEM -> MemoryError - 340 ENOSYS -> NotImplementedError - 35 ERANGE -> OverflowError - -''' +# See this command for the count of POSIX codes used: +# +# egrep -IR 'AVERROR\(E[A-Z]+\)' vendor/ffmpeg-4.2 |\ +# sed -E 's/.*AVERROR\((E[A-Z]+)\).*/\1/' | \ +# sort | uniq -c +# +# The biggest ones that don't map to PEP 3151 builtins: +# +# 2106 EINVAL -> ValueError +# 649 EIO -> IOError (if it is distinct from OSError) +# 4080 ENOMEM -> MemoryError +# 340 ENOSYS -> NotImplementedError +# 35 ERANGE -> OverflowError classes = {} @@ -237,35 +230,35 @@ def _extend_builtin(name, codes): # PEP 3151 builtins. -_extend_builtin('PermissionError', (errno.EACCES, errno.EPERM)) -_extend_builtin('BlockingIOError', (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK)) -_extend_builtin('ChildProcessError', (errno.ECHILD, )) -_extend_builtin('ConnectionAbortedError', (errno.ECONNABORTED, )) -_extend_builtin('ConnectionRefusedError', (errno.ECONNREFUSED, )) -_extend_builtin('ConnectionResetError', (errno.ECONNRESET, )) -_extend_builtin('FileExistsError', (errno.EEXIST, )) -_extend_builtin('InterruptedError', (errno.EINTR, )) -_extend_builtin('IsADirectoryError', (errno.EISDIR, )) -_extend_builtin('FileNotFoundError', (errno.ENOENT, )) -_extend_builtin('NotADirectoryError', (errno.ENOTDIR, )) -_extend_builtin('BrokenPipeError', (errno.EPIPE, errno.ESHUTDOWN)) -_extend_builtin('ProcessLookupError', (errno.ESRCH, )) -_extend_builtin('TimeoutError', (errno.ETIMEDOUT, )) +_extend_builtin("PermissionError", (errno.EACCES, errno.EPERM)) +_extend_builtin("BlockingIOError", (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK)) +_extend_builtin("ChildProcessError", (errno.ECHILD, )) +_extend_builtin("ConnectionAbortedError", (errno.ECONNABORTED, )) +_extend_builtin("ConnectionRefusedError", (errno.ECONNREFUSED, )) +_extend_builtin("ConnectionResetError", (errno.ECONNRESET, )) +_extend_builtin("FileExistsError", (errno.EEXIST, )) +_extend_builtin("InterruptedError", (errno.EINTR, )) +_extend_builtin("IsADirectoryError", (errno.EISDIR, )) +_extend_builtin("FileNotFoundError", (errno.ENOENT, )) +_extend_builtin("NotADirectoryError", (errno.ENOTDIR, )) +_extend_builtin("BrokenPipeError", (errno.EPIPE, errno.ESHUTDOWN)) +_extend_builtin("ProcessLookupError", (errno.ESRCH, )) +_extend_builtin("TimeoutError", (errno.ETIMEDOUT, )) # Other obvious ones. -_extend_builtin('ValueError', (errno.EINVAL, )) -_extend_builtin('MemoryError', (errno.ENOMEM, )) -_extend_builtin('NotImplementedError', (errno.ENOSYS, )) -_extend_builtin('OverflowError', (errno.ERANGE, )) +_extend_builtin("ValueError", (errno.EINVAL, )) +_extend_builtin("MemoryError", (errno.ENOMEM, )) +_extend_builtin("NotImplementedError", (errno.ENOSYS, )) +_extend_builtin("OverflowError", (errno.ERANGE, )) if IOError is not OSError: - _extend_builtin('IOError', (errno.EIO, )) + _extend_builtin("IOError", (errno.EIO, )) # The rest of them (for now) -_extend_builtin('OSError', [code for code in errno.errorcode if code not in classes]) +_extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) # Classes for the FFmpeg errors. for enum_name, code, name, base in _ffmpeg_specs: - name = name or enum_name.title().replace('_', '') + 'Error' + name = name or enum_name.title().replace("_", "") + "Error" if base is None: bases = (FFmpegError, ) @@ -289,12 +282,11 @@ cdef object _local = local() cdef int _err_count = 0 cdef int stash_exception(exc_info=None): - global _err_count - existing = getattr(_local, 'exc_info', None) + existing = getattr(_local, "exc_info", None) if existing is not None: - print >> sys.stderr, 'PyAV library exception being dropped:' + print >> sys.stderr, "PyAV library exception being dropped:" traceback.print_exception(*existing) _err_count -= 1 # Balance out the +=1 that is coming. @@ -316,7 +308,7 @@ cpdef int err_check(int res, filename=None) except -1: # Check for stashed exceptions. if _err_count: - exc_info = getattr(_local, 'exc_info', None) + exc_info = getattr(_local, "exc_info", None) if exc_info is not None: _err_count -= 1 _local.exc_info = None @@ -342,22 +334,19 @@ class UndefinedError(FFmpegError): cpdef make_error(int res, filename=None, log=None): - cdef int code = -res cdef bytes py_buffer cdef char *c_buffer if code == c_PYAV_STASHED_ERROR: message = PYAV_STASHED_ERROR_message - else: - # Jump through some hoops due to Python 2 in same codebase. py_buffer = b"\0" * lib.AV_ERROR_MAX_STRING_SIZE c_buffer = py_buffer lib.av_strerror(res, c_buffer, lib.AV_ERROR_MAX_STRING_SIZE) py_buffer = c_buffer - message = py_buffer.decode('latin1') + message = py_buffer.decode("latin1") # Default to the OS if we have no message; this should not get called. message = message or os.strerror(code) diff --git a/av/filter/__init__.pyi b/av/filter/__init__.pyi new file mode 100644 index 000000000..8a6b5a59b --- /dev/null +++ b/av/filter/__init__.pyi @@ -0,0 +1,3 @@ +from .context import * +from .filter import * +from .graph import * diff --git a/av/filter/context.pyi b/av/filter/context.pyi new file mode 100644 index 000000000..0d70c0095 --- /dev/null +++ b/av/filter/context.pyi @@ -0,0 +1,15 @@ +from av.frame import Frame + +from .pad import FilterContextPad + +class FilterContext: + name: str | None + inputs: tuple[FilterContextPad, ...] + outputs: tuple[FilterContextPad, ...] + + def init(self, args: str | None = None, **kwargs: str | None) -> None: ... + def link_to( + self, input_: FilterContext, output_idx: int = 0, input_idx: int = 0 + ) -> None: ... + def push(self, frame: Frame) -> None: ... + def pull(self) -> Frame: ... diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 4505c7cd3..54ed710ab 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -20,41 +20,41 @@ cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterC cdef class FilterContext: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot construct FilterContext') + raise RuntimeError("cannot construct FilterContext") def __repr__(self): - return '' % ( - (repr(self.ptr.name) if self.ptr.name != NULL else '') if self.ptr != NULL else 'None', - self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None, - id(self), - ) - - property name: - def __get__(self): - if self.ptr.name != NULL: - return self.ptr.name - - property inputs: - def __get__(self): - if self._inputs is None: - self._inputs = alloc_filter_pads(self.filter, self.ptr.input_pads, True, self) - return self._inputs - - property outputs: - def __get__(self): - if self._outputs is None: - self._outputs = alloc_filter_pads(self.filter, self.ptr.output_pads, False, self) - return self._outputs + if self.ptr != NULL: + name = repr(self.ptr.name) if self.ptr.name != NULL else "" + else: + name = "None" - def init(self, args=None, **kwargs): + parent = self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None + return f"" + + @property + def name(self): + if self.ptr.name != NULL: + return self.ptr.name + @property + def inputs(self): + if self._inputs is None: + self._inputs = alloc_filter_pads(self.filter, self.ptr.input_pads, True, self) + return self._inputs + + @property + def outputs(self): + if self._outputs is None: + self._outputs = alloc_filter_pads(self.filter, self.ptr.output_pads, False, self) + return self._outputs + + def init(self, args=None, **kwargs): if self.inited: - raise ValueError('already inited') + raise ValueError("already inited") if args and kwargs: - raise ValueError('cannot init from args and kwargs') + raise ValueError("cannot init from args and kwargs") cdef _Dictionary dict_ = None cdef char *c_args = NULL @@ -68,7 +68,7 @@ cdef class FilterContext: self.inited = True if dict_: - raise ValueError('unused config: %s' % ', '.join(sorted(dict_))) + raise ValueError(f"unused config: {', '.join(sorted(dict_))}") def link_to(self, FilterContext input_, int output_idx=0, int input_idx=0): err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) @@ -81,7 +81,7 @@ cdef class FilterContext: res = lib.av_buffersrc_write_frame(self.ptr, NULL) err_check(res) return - elif self.filter.name in ('abuffer', 'buffer'): + elif self.filter.name in ("abuffer", "buffer"): with nogil: res = lib.av_buffersrc_write_frame(self.ptr, frame.ptr) err_check(res) @@ -89,25 +89,29 @@ cdef class FilterContext: # Delegate to the input. if len(self.inputs) != 1: - raise ValueError('cannot delegate push without single input; found %d' % len(self.inputs)) + raise ValueError( + f"cannot delegate push without single input; found {len(self.inputs)}" + ) if not self.inputs[0].link: - raise ValueError('cannot delegate push without linked input') + raise ValueError("cannot delegate push without linked input") self.inputs[0].linked.context.push(frame) def pull(self): cdef Frame frame cdef int res - if self.filter.name == 'buffersink': + if self.filter.name == "buffersink": frame = alloc_video_frame() - elif self.filter.name == 'abuffersink': + elif self.filter.name == "abuffersink": frame = alloc_audio_frame() else: # Delegate to the output. if len(self.outputs) != 1: - raise ValueError('cannot delegate pull without single output; found %d' % len(self.outputs)) + raise ValueError( + f"cannot delegate pull without single output; found {len(self.outputs)}" + ) if not self.outputs[0].link: - raise ValueError('cannot delegate pull without linked output') + raise ValueError("cannot delegate pull without linked output") return self.outputs[0].linked.context.pull() self.graph.configure() diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi new file mode 100644 index 000000000..f94c79e1c --- /dev/null +++ b/av/filter/filter.pyi @@ -0,0 +1,21 @@ +from av.descriptor import Descriptor +from av.option import Option + +from .pad import FilterPad + +class Filter: + name: str + description: str + + descriptor: Descriptor + options: tuple[Option, ...] | None + flags: int + dynamic_inputs: bool + dynamic_outputs: bool + timeline_support: bool + slice_threads: bool + command_support: bool + inputs: tuple[FilterPad, ...] + outputs: tuple[FilterPad, ...] + +filters_available: set[str] diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index 57090a47d..d4880dc15 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -22,71 +22,70 @@ cpdef enum FilterFlags: cdef class Filter: - def __cinit__(self, name): if name is _cinit_sentinel: return if not isinstance(name, str): - raise TypeError('takes a filter name as a string') + raise TypeError("takes a filter name as a string") self.ptr = lib.avfilter_get_by_name(name) if not self.ptr: - raise ValueError('no filter %s' % name) - - property descriptor: - def __get__(self): - if self._descriptor is None: - self._descriptor = wrap_avclass(self.ptr.priv_class) - return self._descriptor - - property options: - def __get__(self): - if self.descriptor is None: - return - return self.descriptor.options - - property name: - def __get__(self): - return self.ptr.name - - property description: - def __get__(self): - return self.ptr.description - - property flags: - def __get__(self): - return self.ptr.flags - - property dynamic_inputs: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_INPUTS) - - property dynamic_outputs: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS) - - property timeline_support: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC) - - property slice_threads: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SLICE_THREADS) - - property command_support: - def __get__(self): - return self.ptr.process_command != NULL - - property inputs: - def __get__(self): - if self._inputs is None: - self._inputs = alloc_filter_pads(self, self.ptr.inputs, True) - return self._inputs - - property outputs: - def __get__(self): - if self._outputs is None: - self._outputs = alloc_filter_pads(self, self.ptr.outputs, False) - return self._outputs + raise ValueError(f"no filter {name}") + + @property + def descriptor(self): + if self._descriptor is None: + self._descriptor = wrap_avclass(self.ptr.priv_class) + return self._descriptor + + @property + def options(self): + if self.descriptor is None: + return + return self.descriptor.options + + @property + def name(self): + return self.ptr.name + + @property + def description(self): + return self.ptr.description + + @property + def flags(self): + return self.ptr.flags + + @property + def dynamic_inputs(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_INPUTS) + + @property + def dynamic_outputs(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS) + + @property + def timeline_support(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC) + + @property + def slice_threads(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_SLICE_THREADS) + + @property + def command_support(self): + return self.ptr.process_command != NULL + + @property + def inputs(self): + if self._inputs is None: + self._inputs = alloc_filter_pads(self, self.ptr.inputs, True) + return self._inputs + + @property + def outputs(self): + if self._outputs is None: + self._outputs = alloc_filter_pads(self, self.ptr.outputs, False) + return self._outputs cdef get_filter_names(): diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi new file mode 100644 index 000000000..75930d08a --- /dev/null +++ b/av/filter/graph.pyi @@ -0,0 +1,43 @@ +from fractions import Fraction +from typing import Any + +from av.audio.format import AudioFormat +from av.audio.frame import AudioFrame +from av.audio.layout import AudioLayout +from av.audio.stream import AudioStream +from av.video.format import VideoFormat +from av.video.frame import VideoFrame +from av.video.stream import VideoStream + +from .context import FilterContext +from .filter import Filter + +class Graph: + configured: bool + + def __init__(self) -> None: ... + def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... + def add( + self, filter: str | Filter, args: Any = None, **kwargs: str + ) -> FilterContext: ... + def add_buffer( + self, + template: VideoStream | None = None, + width: int | None = None, + height: int | None = None, + format: VideoFormat | None = None, + name: str | None = None, + time_base: Fraction | None = None, + ) -> FilterContext: ... + def add_abuffer( + self, + template: AudioStream | None = None, + sample_rate: int | None = None, + format: AudioFormat | None = None, + layout: AudioLayout | None = None, + channels: int | None = None, + name: str | None = None, + time_base: Fraction | None = None, + ) -> FilterContext: ... + def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... + def pull(self) -> VideoFrame | AudioFrame: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index bf71fac9e..f6376b3a3 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -1,5 +1,5 @@ -from fractions import Fraction import warnings +from fractions import Fraction from av.audio.format cimport AudioFormat from av.audio.frame cimport AudioFrame @@ -12,9 +12,7 @@ from av.video.frame cimport VideoFrame cdef class Graph: - def __cinit__(self): - self.ptr = lib.avfilter_graph_alloc() self.configured = False self._name_counts = {} @@ -33,7 +31,7 @@ cdef class Graph: count = self._name_counts.get(name, 0) self._name_counts[name] = count + 1 if count: - return '%s_%s' % (name, count) + return "%s_%s" % (name, count) else: return name @@ -41,41 +39,14 @@ cdef class Graph: if self.configured and not force: return - # if auto_buffer: - # for ctx in self._context_by_ptr.itervalues(): - # for in_ in ctx.inputs: - # if not in_.link: - # if in_.type == 'video': - # pass - err_check(lib.avfilter_graph_config(self.ptr, NULL)) self.configured = True # We get auto-inserted stuff here. self._auto_register() - # def parse_string(self, str filter_str): - # err_check(lib.avfilter_graph_parse2(self.ptr, filter_str, &self.inputs, &self.outputs)) - # - # cdef lib.AVFilterInOut *input_ - # while input_ != NULL: - # print 'in ', input_.pad_idx, (input_.name if input_.name != NULL else ''), input_.filter_ctx.name, input_.filter_ctx.filter.name - # input_ = input_.next - # - # cdef lib.AVFilterInOut *output - # while output != NULL: - # print 'out', output.pad_idx, (output.name if output.name != NULL else ''), output.filter_ctx.name, output.filter_ctx.filter.name - # output = output.next - - # NOTE: Only FFmpeg supports this. - # def dump(self): - # cdef char *buf = lib.avfilter_graph_dump(self.ptr, "") - # cdef str ret = buf - # lib.av_free(buf) - # return ret def add(self, filter, args=None, **kwargs): - cdef Filter cy_filter if isinstance(filter, str): cy_filter = Filter(filter) @@ -84,7 +55,7 @@ cdef class Graph: else: raise TypeError("filter must be a string or Filter") - cdef str name = self._get_unique_name(kwargs.pop('name', None) or cy_filter.name) + cdef str name = self._get_unique_name(kwargs.pop("name", None) or cy_filter.name) cdef lib.AVFilterContext *ptr = lib.avfilter_graph_alloc_filter(self.ptr, cy_filter.ptr, name) if not ptr: @@ -124,7 +95,6 @@ cdef class Graph: self._nb_filters_seen = self.ptr.nb_filters def add_buffer(self, template=None, width=None, height=None, format=None, name=None, time_base=None): - if template is not None: if width is None: width = template.width @@ -136,24 +106,24 @@ cdef class Graph: time_base = template.time_base if width is None: - raise ValueError('missing width') + raise ValueError("missing width") if height is None: - raise ValueError('missing height') + raise ValueError("missing height") if format is None: - raise ValueError('missing format') + raise ValueError("missing format") if time_base is None: - warnings.warn('missing time_base. Guessing 1/1000 time base. ' - 'This is deprecated and may be removed in future releases.', + warnings.warn("missing time_base. Guessing 1/1000 time base. " + "This is deprecated and may be removed in future releases.", DeprecationWarning) time_base = Fraction(1, 1000) return self.add( - 'buffer', + "buffer", name=name, - video_size=f'{width}x{height}', + video_size=f"{width}x{height}", pix_fmt=str(int(VideoFormat(format))), time_base=str(time_base), - pixel_aspect='1/1', + pixel_aspect="1/1", ) def add_abuffer(self, template=None, sample_rate=None, format=None, layout=None, channels=None, name=None, time_base=None): @@ -174,11 +144,11 @@ cdef class Graph: time_base = template.time_base if sample_rate is None: - raise ValueError('missing sample_rate') + raise ValueError("missing sample_rate") if format is None: - raise ValueError('missing format') + raise ValueError("missing format") if layout is None and channels is None: - raise ValueError('missing layout or channels') + raise ValueError("missing layout or channels") if time_base is None: time_base = Fraction(1, sample_rate) @@ -188,35 +158,33 @@ cdef class Graph: time_base=str(time_base), ) if layout: - kwargs['channel_layout'] = AudioLayout(layout).name + kwargs["channel_layout"] = AudioLayout(layout).name if channels: - kwargs['channels'] = str(channels) + kwargs["channels"] = str(channels) - return self.add('abuffer', name=name, **kwargs) + return self.add("abuffer", name=name, **kwargs) def push(self, frame): - if frame is None: - contexts = self._context_by_type.get('buffer', []) + self._context_by_type.get('abuffer', []) + contexts = self._context_by_type.get("buffer", []) + self._context_by_type.get("abuffer", []) elif isinstance(frame, VideoFrame): - contexts = self._context_by_type.get('buffer', []) + contexts = self._context_by_type.get("buffer", []) elif isinstance(frame, AudioFrame): - contexts = self._context_by_type.get('abuffer', []) + contexts = self._context_by_type.get("abuffer", []) else: - raise ValueError('can only AudioFrame, VideoFrame or None; got %s' % type(frame)) + raise ValueError(f"can only AudioFrame, VideoFrame or None; got {type(frame)}") if len(contexts) != 1: - raise ValueError('can only auto-push with single buffer; found %s' % len(contexts)) + raise ValueError(f"can only auto-push with single buffer; found {len(contexts)}") contexts[0].push(frame) def pull(self): - - vsinks = self._context_by_type.get('buffersink', []) - asinks = self._context_by_type.get('abuffersink', []) + vsinks = self._context_by_type.get("buffersink", []) + asinks = self._context_by_type.get("abuffersink", []) nsinks = len(vsinks) + len(asinks) if nsinks != 1: - raise ValueError('can only auto-pull with single sink; found %s' % nsinks) + raise ValueError(f"can only auto-pull with single sink; found {nsinks}") return (vsinks or asinks)[0].pull() diff --git a/av/filter/link.pyi b/av/filter/link.pyi new file mode 100644 index 000000000..dd420ad91 --- /dev/null +++ b/av/filter/link.pyi @@ -0,0 +1,5 @@ +from .pad import FilterContextPad + +class FilterLink: + input: FilterContextPad + output: FilterContextPad diff --git a/av/filter/link.pyx b/av/filter/link.pyx index c99e27f3b..78b7da30f 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.pyx @@ -10,40 +10,40 @@ cdef class FilterLink: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot instantiate FilterLink') - - property input: - def __get__(self): - if self._input: - return self._input - cdef lib.AVFilterContext *cctx = self.ptr.src - cdef unsigned int i - for i in range(cctx.nb_outputs): - if self.ptr == cctx.outputs[i]: - break - else: - raise RuntimeError('could not find link in context') - ctx = self.graph._context_by_ptr[cctx] - self._input = ctx.outputs[i] - return self._input + raise RuntimeError("cannot instantiate FilterLink") - property output: - def __get__(self): - if self._output: - return self._output - cdef lib.AVFilterContext *cctx = self.ptr.dst - cdef unsigned int i - for i in range(cctx.nb_inputs): - if self.ptr == cctx.inputs[i]: - break - else: - raise RuntimeError('could not find link in context') - try: - ctx = self.graph._context_by_ptr[cctx] - except KeyError: - raise RuntimeError('could not find context in graph', (cctx.name, cctx.filter.name)) - self._output = ctx.inputs[i] + @property + def input(self): + if self._input: + return self._input + cdef lib.AVFilterContext *cctx = self.ptr.src + cdef unsigned int i + for i in range(cctx.nb_outputs): + if self.ptr == cctx.outputs[i]: + break + else: + raise RuntimeError("could not find link in context") + ctx = self.graph._context_by_ptr[cctx] + self._input = ctx.outputs[i] + return self._input + + @property + def output(self): + if self._output: return self._output + cdef lib.AVFilterContext *cctx = self.ptr.dst + cdef unsigned int i + for i in range(cctx.nb_inputs): + if self.ptr == cctx.inputs[i]: + break + else: + raise RuntimeError("could not find link in context") + try: + ctx = self.graph._context_by_ptr[cctx] + except KeyError: + raise RuntimeError("could not find context in graph", (cctx.name, cctx.filter.name)) + self._output = ctx.inputs[i] + return self._output cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr): diff --git a/av/filter/pad.pyi b/av/filter/pad.pyi new file mode 100644 index 000000000..1a6c9bda6 --- /dev/null +++ b/av/filter/pad.pyi @@ -0,0 +1,10 @@ +from .link import FilterLink + +class FilterPad: + is_output: bool + name: str + type: str + +class FilterContextPad(FilterPad): + link: FilterLink | None + linked: FilterContextPad | None diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 482b2fc36..873b31b04 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -5,27 +5,23 @@ cdef object _cinit_sentinel = object() cdef class FilterPad: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot construct FilterPad') + raise RuntimeError("cannot construct FilterPad") def __repr__(self): - return '' % ( - self.filter.name, - 'inputs' if self.is_input else 'outputs', - self.index, - self.name, - self.type, - ) - - property is_output: - def __get__(self): - return not self.is_input - - property name: - def __get__(self): - return lib.avfilter_pad_get_name(self.base_ptr, self.index) + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + + return f"" + + @property + def is_output(self): + return not self.is_input + + @property + def name(self): + return lib.avfilter_pad_get_name(self.base_ptr, self.index) @property def type(self): @@ -40,38 +36,32 @@ cdef class FilterPad: cdef class FilterContextPad(FilterPad): - def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + context = self.context.name + + return f"" - return '' % ( - self.filter.name, - 'inputs' if self.is_input else 'outputs', - self.index, - self.context.name, - self.name, - self.type, - ) - - property link: - def __get__(self): - if self._link: - return self._link - cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs - cdef lib.AVFilterLink *link = links[self.index] - if not link: - return - self._link = wrap_filter_link(self.context.graph, link) + @property + def link(self): + if self._link: return self._link + cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs + cdef lib.AVFilterLink *link = links[self.index] + if not link: + return + self._link = wrap_filter_link(self.context.graph, link) + return self._link - property linked: - def __get__(self): - cdef FilterLink link = self.link - if link: - return link.input if self.is_input else link.output + @property + def linked(self): + cdef FilterLink link = self.link + if link: + return link.input if self.is_input else link.output cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): - if not ptr: return () diff --git a/av/format.pyi b/av/format.pyi new file mode 100644 index 000000000..874920928 --- /dev/null +++ b/av/format.pyi @@ -0,0 +1,48 @@ +__all__ = ("ContainerFormat", "formats_available") + +from .enum import EnumFlag + +class Flags(EnumFlag): + NOFILE: int + NEEDNUMBER: int + SHOW_IDS: int + GLOBALHEADER: int + NOTIMESTAMPS: int + GENERIC_INDEX: int + TS_DISCONT: int + VARIABLE_FPS: int + NODIMENSIONS: int + NOSTREAMS: int + NOBINSEARCH: int + NOGENSEARCH: int + NO_BYTE_SEEK: int + ALLOW_FLUSH: int + TS_NONSTRICT: int + TS_NEGATIVE: int + SEEK_TO_PTS: int + +class ContainerFormat: + is_input: bool + is_output: bool + long_name: str + + # flags + no_file: int + need_number: int + show_ids: int + global_header: int + no_timestamps: int + generic_index: int + ts_discont: int + variable_fps: int + no_dimensions: int + no_streams: int + no_bin_search: int + no_gen_search: int + no_byte_seek: int + allow_flush: int + ts_nonstrict: int + ts_negative: int + seek_to_pts: int + +formats_available: set[str] diff --git a/av/format.pyx b/av/format.pyx index 7afe4416f..06b533cf4 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -8,7 +8,7 @@ cdef object _cinit_bypass_sentinel = object() cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutputFormat* optr): if not iptr and not optr: - raise ValueError('needs input format or output format') + raise ValueError("needs input format or output format") cdef ContainerFormat format = ContainerFormat.__new__(ContainerFormat, _cinit_bypass_sentinel) format.iptr = iptr format.optr = optr @@ -16,46 +16,36 @@ cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutpu return format -Flags = define_enum('Flags', __name__, ( - ('NOFILE', lib.AVFMT_NOFILE), - ('NEEDNUMBER', lib.AVFMT_NEEDNUMBER, - """Needs '%d' in filename."""), - ('SHOW_IDS', lib.AVFMT_SHOW_IDS, - """Show format stream IDs numbers."""), - ('GLOBALHEADER', lib.AVFMT_GLOBALHEADER, - """Format wants global header."""), - ('NOTIMESTAMPS', lib.AVFMT_NOTIMESTAMPS, - """Format does not need / have any timestamps."""), - ('GENERIC_INDEX', lib.AVFMT_GENERIC_INDEX, - """Use generic index building code."""), - ('TS_DISCONT', lib.AVFMT_TS_DISCONT, +Flags = define_enum("Flags", __name__, ( + ("NOFILE", lib.AVFMT_NOFILE), + ("NEEDNUMBER", lib.AVFMT_NEEDNUMBER, "Needs '%d' in filename."), + ("SHOW_IDS", lib.AVFMT_SHOW_IDS, "Show format stream IDs numbers."), + ("GLOBALHEADER", lib.AVFMT_GLOBALHEADER, "Format wants global header."), + ("NOTIMESTAMPS", lib.AVFMT_NOTIMESTAMPS, "Format does not need / have any timestamps."), + ("GENERIC_INDEX", lib.AVFMT_GENERIC_INDEX, "Use generic index building code."), + ("TS_DISCONT", lib.AVFMT_TS_DISCONT, """Format allows timestamp discontinuities. Note, muxers always require valid (monotone) timestamps"""), - ('VARIABLE_FPS', lib.AVFMT_VARIABLE_FPS, - """Format allows variable fps."""), - ('NODIMENSIONS', lib.AVFMT_NODIMENSIONS, - """Format does not need width/height"""), - ('NOSTREAMS', lib.AVFMT_NOSTREAMS, - """Format does not require any streams"""), - ('NOBINSEARCH', lib.AVFMT_NOBINSEARCH, - """Format does not allow to fall back on binary search via read_timestamp"""), - ('NOGENSEARCH', lib.AVFMT_NOGENSEARCH, - """Format does not allow to fall back on generic search"""), - ('NO_BYTE_SEEK', lib.AVFMT_NO_BYTE_SEEK, - """Format does not allow seeking by bytes"""), - ('ALLOW_FLUSH', lib.AVFMT_ALLOW_FLUSH, + ("VARIABLE_FPS", lib.AVFMT_VARIABLE_FPS, "Format allows variable fps."), + ("NODIMENSIONS", lib.AVFMT_NODIMENSIONS, "Format does not need width/height"), + ("NOSTREAMS", lib.AVFMT_NOSTREAMS, "Format does not require any streams"), + ("NOBINSEARCH", lib.AVFMT_NOBINSEARCH, + "Format does not allow to fall back on binary search via read_timestamp"), + ("NOGENSEARCH", lib.AVFMT_NOGENSEARCH, + "Format does not allow to fall back on generic search"), + ("NO_BYTE_SEEK", lib.AVFMT_NO_BYTE_SEEK, "Format does not allow seeking by bytes"), + ("ALLOW_FLUSH", lib.AVFMT_ALLOW_FLUSH, """Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function."""), - ('TS_NONSTRICT', lib.AVFMT_TS_NONSTRICT, + ("TS_NONSTRICT", lib.AVFMT_TS_NONSTRICT, """Format does not require strictly increasing timestamps, but they must still be monotonic."""), - ('TS_NEGATIVE', lib.AVFMT_TS_NEGATIVE, + ("TS_NEGATIVE", lib.AVFMT_TS_NEGATIVE, """Format allows muxing negative timestamps. If not set the timestamp will be shifted in av_write_frame and av_interleaved_write_frame so they start from 0. The user or muxer can override this through AVFormatContext.avoid_negative_ts"""), - ('SEEK_TO_PTS', lib.AVFMT_SEEK_TO_PTS, - """Seeking is based on PTS"""), + ("SEEK_TO_PTS", lib.AVFMT_SEEK_TO_PTS, "Seeking is based on PTS"), ), is_flags=True) @@ -70,7 +60,6 @@ cdef class ContainerFormat: """ def __cinit__(self, name, mode=None): - if name is _cinit_bypass_sentinel: return @@ -80,71 +69,71 @@ cdef class ContainerFormat: self.name = name # Searches comma-seperated names. - if mode is None or mode == 'r': + if mode is None or mode == "r": self.iptr = lib.av_find_input_format(name) - if mode is None or mode == 'w': + if mode is None or mode == "w": self.optr = lib.av_guess_format(name, NULL, NULL) if not self.iptr and not self.optr: - raise ValueError('no container format %r' % name) + raise ValueError(f"no container format {name!r}") def __repr__(self): - return '' % (self.__class__.__name__, self.name) + return f"" - property descriptor: - def __get__(self): - if self.iptr: - return wrap_avclass(self.iptr.priv_class) - else: - return wrap_avclass(self.optr.priv_class) + @property + def descriptor(self): + if self.iptr: + return wrap_avclass(self.iptr.priv_class) + else: + return wrap_avclass(self.optr.priv_class) - property options: - def __get__(self): - return self.descriptor.options + @property + def options(self): + return self.descriptor.options - property input: + @property + def input(self): """An input-only view of this format.""" - def __get__(self): - if self.iptr == NULL: - return None - elif self.optr == NULL: - return self - else: - return build_container_format(self.iptr, NULL) - - property output: + if self.iptr == NULL: + return None + elif self.optr == NULL: + return self + else: + return build_container_format(self.iptr, NULL) + + @property + def output(self): """An output-only view of this format.""" - def __get__(self): - if self.optr == NULL: - return None - elif self.iptr == NULL: - return self - else: - return build_container_format(NULL, self.optr) - - property is_input: - def __get__(self): - return self.iptr != NULL - - property is_output: - def __get__(self): - return self.optr != NULL - - property long_name: - def __get__(self): - # We prefer the output names since the inputs may represent - # multiple formats. - return self.optr.long_name if self.optr else self.iptr.long_name - - property extensions: - def __get__(self): - cdef set exts = set() - if self.iptr and self.iptr.extensions: - exts.update(self.iptr.extensions.split(',')) - if self.optr and self.optr.extensions: - exts.update(self.optr.extensions.split(',')) - return exts + if self.optr == NULL: + return None + elif self.iptr == NULL: + return self + else: + return build_container_format(NULL, self.optr) + + @property + def is_input(self): + return self.iptr != NULL + + @property + def is_output(self): + return self.optr != NULL + + @property + def long_name(self): + # We prefer the output names since the inputs may represent + # multiple formats. + return self.optr.long_name if self.optr else self.iptr.long_name + + @property + def extensions(self): + cdef set exts = set() + if self.iptr and self.iptr.extensions: + exts.update(self.iptr.extensions.split(",")) + if self.optr and self.optr.extensions: + exts.update(self.optr.extensions.split(",")) + return exts @Flags.property def flags(self): @@ -153,23 +142,23 @@ cdef class ContainerFormat: (self.optr.flags if self.optr else 0) ) - no_file = flags.flag_property('NOFILE') - need_number = flags.flag_property('NEEDNUMBER') - show_ids = flags.flag_property('SHOW_IDS') - global_header = flags.flag_property('GLOBALHEADER') - no_timestamps = flags.flag_property('NOTIMESTAMPS') - generic_index = flags.flag_property('GENERIC_INDEX') - ts_discont = flags.flag_property('TS_DISCONT') - variable_fps = flags.flag_property('VARIABLE_FPS') - no_dimensions = flags.flag_property('NODIMENSIONS') - no_streams = flags.flag_property('NOSTREAMS') - no_bin_search = flags.flag_property('NOBINSEARCH') - no_gen_search = flags.flag_property('NOGENSEARCH') - no_byte_seek = flags.flag_property('NO_BYTE_SEEK') - allow_flush = flags.flag_property('ALLOW_FLUSH') - ts_nonstrict = flags.flag_property('TS_NONSTRICT') - ts_negative = flags.flag_property('TS_NEGATIVE') - seek_to_pts = flags.flag_property('SEEK_TO_PTS') + no_file = flags.flag_property("NOFILE") + need_number = flags.flag_property("NEEDNUMBER") + show_ids = flags.flag_property("SHOW_IDS") + global_header = flags.flag_property("GLOBALHEADER") + no_timestamps = flags.flag_property("NOTIMESTAMPS") + generic_index = flags.flag_property("GENERIC_INDEX") + ts_discont = flags.flag_property("TS_DISCONT") + variable_fps = flags.flag_property("VARIABLE_FPS") + no_dimensions = flags.flag_property("NODIMENSIONS") + no_streams = flags.flag_property("NOSTREAMS") + no_bin_search = flags.flag_property("NOBINSEARCH") + no_gen_search = flags.flag_property("NOGENSEARCH") + no_byte_seek = flags.flag_property("NO_BYTE_SEEK") + allow_flush = flags.flag_property("ALLOW_FLUSH") + ts_nonstrict = flags.flag_property("TS_NONSTRICT") + ts_negative = flags.flag_property("TS_NEGATIVE") + seek_to_pts = flags.flag_property("SEEK_TO_PTS") cdef get_output_format_names(): diff --git a/av/frame.pyi b/av/frame.pyi new file mode 100644 index 000000000..8e81a8198 --- /dev/null +++ b/av/frame.pyi @@ -0,0 +1,9 @@ +from fractions import Fraction + +class Frame: + dts: int | None + pts: int | None + time: float | None + time_base: Fraction + is_corrupt: bool + side_data: dict[str, str] diff --git a/av/frame.pyx b/av/frame.pyx index b98624cc5..1c8fb357a 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -21,12 +21,7 @@ cdef class Frame: lib.av_frame_free(&self.ptr) def __repr__(self): - return 'av.%s #%d pts=%s at 0x%x>' % ( - self.__class__.__name__, - self.index, - self.pts, - id(self), - ) + return f"av.{self.__class__.__name__} #{self.index} pts={self.pts} at 0x{id(self):x}>" cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): """Mimic another frame.""" @@ -45,9 +40,8 @@ cdef class Frame: pass # Dummy to match the API of the others. cdef _rebase_time(self, lib.AVRational dst): - if not dst.num: - raise ValueError('Cannot rebase to zero time.') + raise ValueError("Cannot rebase to zero time.") if not self._time_base.num: self._time_base = dst @@ -57,25 +51,32 @@ cdef class Frame: return if self.ptr.pts != lib.AV_NOPTS_VALUE: - self.ptr.pts = lib.av_rescale_q( - self.ptr.pts, - self._time_base, dst - ) + self.ptr.pts = lib.av_rescale_q(self.ptr.pts, self._time_base, dst) self._time_base = dst - property dts: + @property + def dts(self): """ - The decoding timestamp in :attr:`time_base` units for this frame. + The decoding timestamp copied from the :class:`~av.packet.Packet` that triggered returning this frame in :attr:`time_base` units. + + (if frame threading isn't used) This is also the Presentation time of this frame calculated from only :attr:`.Packet.dts` values without pts values. :type: int """ - def __get__(self): - if self.ptr.pkt_dts == lib.AV_NOPTS_VALUE: - return None - return self.ptr.pkt_dts + if self.ptr.pkt_dts == lib.AV_NOPTS_VALUE: + return None + return self.ptr.pkt_dts - property pts: + @dts.setter + def dts(self, value): + if value is None: + self.ptr.pkt_dts = lib.AV_NOPTS_VALUE + else: + self.ptr.pkt_dts = value + + @property + def pts(self): """ The presentation timestamp in :attr:`time_base` units for this frame. @@ -83,18 +84,19 @@ cdef class Frame: :type: int """ - def __get__(self): - if self.ptr.pts == lib.AV_NOPTS_VALUE: - return None - return self.ptr.pts - - def __set__(self, value): - if value is None: - self.ptr.pts = lib.AV_NOPTS_VALUE - else: - self.ptr.pts = value - - property time: + if self.ptr.pts == lib.AV_NOPTS_VALUE: + return None + return self.ptr.pts + + @pts.setter + def pts(self, value): + if value is None: + self.ptr.pts = lib.AV_NOPTS_VALUE + else: + self.ptr.pts = value + + @property + def time(self): """ The presentation time in seconds for this frame. @@ -102,32 +104,33 @@ cdef class Frame: :type: float """ - def __get__(self): - if self.ptr.pts == lib.AV_NOPTS_VALUE: - return None - else: - return float(self.ptr.pts) * self._time_base.num / self._time_base.den + if self.ptr.pts == lib.AV_NOPTS_VALUE: + return None + else: + return float(self.ptr.pts) * self._time_base.num / self._time_base.den - property time_base: + @property + def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. :type: fractions.Fraction """ - def __get__(self): - if self._time_base.num: - return avrational_to_fraction(&self._time_base) + if self._time_base.num: + return avrational_to_fraction(&self._time_base) - def __set__(self, value): - to_avrational(value, &self._time_base) + @time_base.setter + def time_base(self, value): + to_avrational(value, &self._time_base) - property is_corrupt: + @property + def is_corrupt(self): """ Is this frame corrupt? :type: bool """ - def __get__(self): return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) + return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) @property def side_data(self): diff --git a/av/logging.pyi b/av/logging.pyi new file mode 100644 index 000000000..a85eba2df --- /dev/null +++ b/av/logging.pyi @@ -0,0 +1,46 @@ +import logging +from threading import Lock +from typing import Any, Callable + +PANIC: int +FATAL: int +ERROR: int +WARNING: int +INFO: int +VERBOSE: int +DEBUG: int +TRACE: int +CRITICAL: int + +def adapt_level(level: int) -> int: ... +def get_level() -> int: ... +def set_level(level: int) -> None: ... +def restore_default_callback() -> None: ... +def get_print_after_shutdown() -> bool: ... +def set_print_after_shutdown(v: bool) -> None: ... +def get_skip_repeated() -> bool: ... +def set_skip_repeated(v: bool) -> None: ... +def get_last_error() -> tuple[int, tuple[int, str, str] | None]: ... +def log(level: int, name: str, message: str) -> None: ... + +class Capture: + logs: list[tuple[int, str, str]] + + def __init__(self, local: bool = True) -> None: ... + def __enter__(self) -> list[tuple[int, str, str]]: ... + def __exit__( + self, + type_: type | None, + value: Exception | None, + traceback: Callable[..., Any] | None, + ) -> None: ... + +level_threshold: int +print_after_shutdown: bool +skip_repeated: bool +skip_lock: Lock +last_log: tuple[int, str, str] | None +skip_count: int +last_error: tuple[int, str, str] | None +global_captures: list[list[tuple[int, str, str]]] +thread_captures: dict[int, list[tuple[int, str, str]]] diff --git a/av/logging.pyx b/av/logging.pyx index 131b9a69f..8940c3139 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -32,15 +32,14 @@ API Reference from __future__ import absolute_import +cimport libav as lib from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc -cimport libav as lib -from threading import Lock, get_ident import logging import os import sys - +from threading import Lock, get_ident # Library levels. # QUIET = lib.AV_LOG_QUIET # -8; not really a level. @@ -82,8 +81,8 @@ cpdef adapt_level(int level): cdef int level_threshold = lib.AV_LOG_VERBOSE # ... but lets limit ourselves to WARNING (assuming nobody already did this). -if 'libav' not in logging.Logger.manager.loggerDict: - logging.getLogger('libav').setLevel(logging.WARNING) +if "libav" not in logging.Logger.manager.loggerDict: + logging.getLogger("libav").setLevel(logging.WARNING) def get_level(): @@ -169,7 +168,6 @@ cdef global_captures = [] cdef thread_captures = {} cdef class Capture: - """A context manager for capturing logs. :param bool local: Should logs from all threads be captured, or just one @@ -188,7 +186,6 @@ cdef class Capture: cdef list captures def __init__(self, bint local=True): - self.logs = [] if local: @@ -259,7 +256,6 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg return with gil: - try: log_callback_gil(level, name, message) @@ -272,14 +268,13 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg cdef log_callback_gil(int level, const char *c_name, const char *c_message): - global error_count global skip_count global last_log global last_error - name = c_name if c_name is not NULL else '' - message = (c_message).decode('utf8', 'backslashreplace') + name = c_name if c_name is not NULL else "" + message = (c_message).decode("utf8", "backslashreplace") log = (level, name, message) # We have to filter it ourselves, but we will still process it in general so @@ -294,9 +289,7 @@ cdef log_callback_gil(int level, const char *c_name, const char *c_message): cdef object repeat_log = None with skip_lock: - if is_interesting: - is_repeated = skip_repeated and last_log == log if is_repeated: @@ -329,7 +322,6 @@ cdef log_callback_gil(int level, const char *c_name, const char *c_message): cdef log_callback_emit(log): - lib_level, name, message = log captures = thread_captures.get(get_ident()) or global_captures @@ -339,7 +331,7 @@ cdef log_callback_emit(log): py_level = adapt_level(lib_level) - logger_name = 'libav.' + name if name else 'libav.generic' + logger_name = "libav." + name if name else "libav.generic" logger = logging.getLogger(logger_name) logger.log(py_level, message.strip()) @@ -347,5 +339,5 @@ cdef log_callback_emit(log): # Start the magic! # We allow the user to fully disable the logging system as it will not play # nicely with subinterpreters due to FFmpeg-created threads. -if os.environ.get('PYAV_LOGGING') != 'off': +if os.environ.get("PYAV_LOGGING") != "off": lib.av_log_set_callback(log_callback) diff --git a/av/option.pyi b/av/option.pyi new file mode 100644 index 000000000..b7ba670f2 --- /dev/null +++ b/av/option.pyi @@ -0,0 +1,54 @@ +from .enum import EnumFlag, EnumItem + +class OptionType(EnumItem): + FLAGS: int + INT: int + INT64: int + DOUBLE: int + FLOAT: int + STRING: int + RATIONAL: int + BINARY: int + DICT: int + CONST: int + IMAGE_SIZE: int + PIXEL_FMT: int + SAMPLE_FMT: int + VIDEO_RATE: int + DURATION: int + COLOR: int + CHANNEL_LAYOUT: int + BOOL: int + +class OptionFlags(EnumFlag): + ENCODING_PARAM: int + DECODING_PARAM: int + AUDIO_PARAM: int + VIDEO_PARAM: int + SUBTITLE_PARAM: int + EXPORT: int + READONLY: int + FILTERING_PARAM: int + +class BaseOption: + name: str + help: str + flags: int + is_encoding_param: bool + is_decoding_param: bool + is_audio_param: bool + is_video_param: bool + is_subtitle_param: bool + is_export: bool + is_readonly: bool + is_filtering_param: bool + +class Option(BaseOption): + type: OptionType + offset: int + default: int + min: int + max: int + +class OptionChoice(BaseOption): + value: int diff --git a/av/option.pyx b/av/option.pyx index 731a6d508..6ffba50fa 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -15,26 +15,26 @@ cdef Option wrap_option(tuple choices, const lib.AVOption *ptr): return obj -OptionType = define_enum('OptionType', __name__, ( - ('FLAGS', lib.AV_OPT_TYPE_FLAGS), - ('INT', lib.AV_OPT_TYPE_INT), - ('INT64', lib.AV_OPT_TYPE_INT64), - ('DOUBLE', lib.AV_OPT_TYPE_DOUBLE), - ('FLOAT', lib.AV_OPT_TYPE_FLOAT), - ('STRING', lib.AV_OPT_TYPE_STRING), - ('RATIONAL', lib.AV_OPT_TYPE_RATIONAL), - ('BINARY', lib.AV_OPT_TYPE_BINARY), - ('DICT', lib.AV_OPT_TYPE_DICT), - # ('UINT64', lib.AV_OPT_TYPE_UINT64), # Added recently, and not yet used AFAICT. - ('CONST', lib.AV_OPT_TYPE_CONST), - ('IMAGE_SIZE', lib.AV_OPT_TYPE_IMAGE_SIZE), - ('PIXEL_FMT', lib.AV_OPT_TYPE_PIXEL_FMT), - ('SAMPLE_FMT', lib.AV_OPT_TYPE_SAMPLE_FMT), - ('VIDEO_RATE', lib.AV_OPT_TYPE_VIDEO_RATE), - ('DURATION', lib.AV_OPT_TYPE_DURATION), - ('COLOR', lib.AV_OPT_TYPE_COLOR), - ('CHANNEL_LAYOUT', lib.AV_OPT_TYPE_CHANNEL_LAYOUT), - ('BOOL', lib.AV_OPT_TYPE_BOOL), +OptionType = define_enum("OptionType", __name__, ( + ("FLAGS", lib.AV_OPT_TYPE_FLAGS), + ("INT", lib.AV_OPT_TYPE_INT), + ("INT64", lib.AV_OPT_TYPE_INT64), + ("DOUBLE", lib.AV_OPT_TYPE_DOUBLE), + ("FLOAT", lib.AV_OPT_TYPE_FLOAT), + ("STRING", lib.AV_OPT_TYPE_STRING), + ("RATIONAL", lib.AV_OPT_TYPE_RATIONAL), + ("BINARY", lib.AV_OPT_TYPE_BINARY), + ("DICT", lib.AV_OPT_TYPE_DICT), + # ("UINT64", lib.AV_OPT_TYPE_UINT64), # Added recently, and not yet used AFAICT. + ("CONST", lib.AV_OPT_TYPE_CONST), + ("IMAGE_SIZE", lib.AV_OPT_TYPE_IMAGE_SIZE), + ("PIXEL_FMT", lib.AV_OPT_TYPE_PIXEL_FMT), + ("SAMPLE_FMT", lib.AV_OPT_TYPE_SAMPLE_FMT), + ("VIDEO_RATE", lib.AV_OPT_TYPE_VIDEO_RATE), + ("DURATION", lib.AV_OPT_TYPE_DURATION), + ("COLOR", lib.AV_OPT_TYPE_COLOR), + ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHANNEL_LAYOUT), + ("BOOL", lib.AV_OPT_TYPE_BOOL), )) cdef tuple _INT_TYPES = ( @@ -48,114 +48,110 @@ cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_BOOL, ) -OptionFlags = define_enum('OptionFlags', __name__, ( - ('ENCODING_PARAM', lib.AV_OPT_FLAG_ENCODING_PARAM), - ('DECODING_PARAM', lib.AV_OPT_FLAG_DECODING_PARAM), - ('AUDIO_PARAM', lib.AV_OPT_FLAG_AUDIO_PARAM), - ('VIDEO_PARAM', lib.AV_OPT_FLAG_VIDEO_PARAM), - ('SUBTITLE_PARAM', lib.AV_OPT_FLAG_SUBTITLE_PARAM), - ('EXPORT', lib.AV_OPT_FLAG_EXPORT), - ('READONLY', lib.AV_OPT_FLAG_READONLY), - ('FILTERING_PARAM', lib.AV_OPT_FLAG_FILTERING_PARAM), +OptionFlags = define_enum("OptionFlags", __name__, ( + ("ENCODING_PARAM", lib.AV_OPT_FLAG_ENCODING_PARAM), + ("DECODING_PARAM", lib.AV_OPT_FLAG_DECODING_PARAM), + ("AUDIO_PARAM", lib.AV_OPT_FLAG_AUDIO_PARAM), + ("VIDEO_PARAM", lib.AV_OPT_FLAG_VIDEO_PARAM), + ("SUBTITLE_PARAM", lib.AV_OPT_FLAG_SUBTITLE_PARAM), + ("EXPORT", lib.AV_OPT_FLAG_EXPORT), + ("READONLY", lib.AV_OPT_FLAG_READONLY), + ("FILTERING_PARAM", lib.AV_OPT_FLAG_FILTERING_PARAM), ), is_flags=True) cdef class BaseOption: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('Cannot construct av.%s' % self.__class__.__name__) + raise RuntimeError(f"Cannot construct av.{self.__class__.__name__}") - property name: - def __get__(self): - return self.ptr.name + @property + def name(self): + return self.ptr.name - property help: - def __get__(self): - return self.ptr.help if self.ptr.help != NULL else '' + @property + def help(self): + return self.ptr.help if self.ptr.help != NULL else "" - property flags: - def __get__(self): - return self.ptr.flags + @property + def flags(self): + return self.ptr.flags # Option flags - property is_encoding_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_ENCODING_PARAM) - property is_decoding_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_DECODING_PARAM) - property is_audio_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_AUDIO_PARAM) - property is_video_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_VIDEO_PARAM) - property is_subtitle_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_SUBTITLE_PARAM) - property is_export: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_EXPORT) - property is_readonly: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_READONLY) - property is_filtering_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_FILTERING_PARAM) + @property + def is_encoding_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_ENCODING_PARAM) + @property + def is_decoding_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_DECODING_PARAM) + @property + def is_audio_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_AUDIO_PARAM) + @property + def is_video_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_VIDEO_PARAM) + @property + def is_subtitle_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_SUBTITLE_PARAM) + @property + def is_export(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_EXPORT) + @property + def is_readonly(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_READONLY) + @property + def is_filtering_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_FILTERING_PARAM) cdef class Option(BaseOption): + @property + def type(self): + return OptionType._get(self.ptr.type, create=True) - property type: - def __get__(self): - return OptionType._get(self.ptr.type, create=True) - - property offset: + @property + def offset(self): """ This can be used to find aliases of an option. Options in a particular descriptor with the same offset are aliases. """ - def __get__(self): - return self.ptr.offset - - property default: - def __get__(self): - if self.ptr.type in _INT_TYPES: - return self.ptr.default_val.i64 - if self.ptr.type in (lib.AV_OPT_TYPE_DOUBLE, lib.AV_OPT_TYPE_FLOAT, - lib.AV_OPT_TYPE_RATIONAL): - return self.ptr.default_val.dbl - if self.ptr.type in (lib.AV_OPT_TYPE_STRING, lib.AV_OPT_TYPE_BINARY, - lib.AV_OPT_TYPE_IMAGE_SIZE, lib.AV_OPT_TYPE_VIDEO_RATE, - lib.AV_OPT_TYPE_COLOR): - return self.ptr.default_val.str if self.ptr.default_val.str != NULL else '' + return self.ptr.offset + + @property + def default(self): + if self.ptr.type in _INT_TYPES: + return self.ptr.default_val.i64 + if self.ptr.type in (lib.AV_OPT_TYPE_DOUBLE, lib.AV_OPT_TYPE_FLOAT, + lib.AV_OPT_TYPE_RATIONAL): + return self.ptr.default_val.dbl + if self.ptr.type in (lib.AV_OPT_TYPE_STRING, lib.AV_OPT_TYPE_BINARY, + lib.AV_OPT_TYPE_IMAGE_SIZE, lib.AV_OPT_TYPE_VIDEO_RATE, + lib.AV_OPT_TYPE_COLOR): + return self.ptr.default_val.str if self.ptr.default_val.str != NULL else "" def _norm_range(self, value): if self.ptr.type in _INT_TYPES: return int(value) return value - property min: - def __get__(self): - return self._norm_range(self.ptr.min) + @property + def min(self): + return self._norm_range(self.ptr.min) - property max: - def __get__(self): - return self._norm_range(self.ptr.max) + @property + def max(self): + return self._norm_range(self.ptr.max) def __repr__(self): - return '' % ( - self.__class__.__name__, - self.name, - self.type, - self.offset, - id(self), + return ( + f"" ) cdef OptionChoice wrap_option_choice(const lib.AVOption *ptr, bint is_default): if ptr == NULL: return None + cdef OptionChoice obj = OptionChoice(_cinit_sentinel) obj.ptr = ptr obj.is_default = is_default @@ -168,9 +164,9 @@ cdef class OptionChoice(BaseOption): choices of non-const option with same unit. """ - property value: - def __get__(self): - return self.ptr.default_val.i64 + @property + def value(self): + return self.ptr.default_val.i64 def __repr__(self): - return '' % (self.__class__.__name__, self.name, id(self)) + return f"" diff --git a/av/packet.pyi b/av/packet.pyi new file mode 100644 index 000000000..cca33009c --- /dev/null +++ b/av/packet.pyi @@ -0,0 +1,24 @@ +from fractions import Fraction +from typing import Iterator + +from av.subtitles.subtitle import SubtitleSet + +from .stream import Stream + +class Packet: + stream: Stream + stream_index: int + time_base: Fraction + pts: int | None + dts: int + pos: int | None + size: int + duration: int | None + is_keyframe: bool + is_corrupt: bool + is_discard: bool + is_trusted: bool + is_disposable: bool + + def __init__(self, input: int | None = None) -> None: ... + def decode(self) -> Iterator[SubtitleSet]: ... diff --git a/av/packet.pyx b/av/packet.pyx index 0687b2237..24fd5581b 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -4,8 +4,6 @@ from av.bytesource cimport bytesource from av.error cimport err_check from av.utils cimport avrational_to_fraction, to_avrational -from av import deprecation - cdef class Packet(Buffer): @@ -21,7 +19,6 @@ cdef class Packet(Buffer): self.ptr = lib.av_packet_alloc() def __init__(self, input=None): - cdef size_t size = 0 cdef ByteSource source = None @@ -48,13 +45,10 @@ cdef class Packet(Buffer): lib.av_packet_free(&self.ptr) def __repr__(self): - return '' % ( - self.__class__.__name__, - self._stream.index if self._stream else 0, - self.dts, - self.pts, - self.ptr.size, - id(self), + stream = self._stream.index if self._stream else 0 + return ( + f"" ) # Buffer protocol. @@ -64,9 +58,8 @@ cdef class Packet(Buffer): return self.ptr.data cdef _rebase_time(self, lib.AVRational dst): - if not dst.num: - raise ValueError('Cannot rebase to zero time.') + raise ValueError("Cannot rebase to zero time.") if not self._time_base.num: self._time_base = dst @@ -86,47 +79,37 @@ cdef class Packet(Buffer): """ return self._stream.decode(self) - @deprecation.method - def decode_one(self): - """ - Send the packet's data to the decoder and return the first decoded frame. - - Returns ``None`` if there is no frame. + @property + def stream_index(self): + return self.ptr.stream_index - .. warning:: This method is deprecated, as it silently discards any - other frames which were decoded. - """ - res = self._stream.decode(self) - return res[0] if res else None - - property stream_index: - def __get__(self): - return self.ptr.stream_index - - property stream: + @property + def stream(self): """ The :class:`Stream` this packet was demuxed from. """ - def __get__(self): - return self._stream + return self._stream - def __set__(self, Stream stream): - self._stream = stream - self.ptr.stream_index = stream.ptr.index + @stream.setter + def stream(self, Stream stream): + self._stream = stream + self.ptr.stream_index = stream.ptr.index - property time_base: + @property + def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. :type: fractions.Fraction """ - def __get__(self): - return avrational_to_fraction(&self._time_base) + return avrational_to_fraction(&self._time_base) - def __set__(self, value): - to_avrational(value, &self._time_base) + @time_base.setter + def time_base(self, value): + to_avrational(value, &self._time_base) - property pts: + @property + def pts(self): """ The presentation timestamp in :attr:`time_base` units for this packet. @@ -134,33 +117,35 @@ cdef class Packet(Buffer): :type: int """ - def __get__(self): - if self.ptr.pts != lib.AV_NOPTS_VALUE: - return self.ptr.pts + if self.ptr.pts != lib.AV_NOPTS_VALUE: + return self.ptr.pts - def __set__(self, v): - if v is None: - self.ptr.pts = lib.AV_NOPTS_VALUE - else: - self.ptr.pts = v + @pts.setter + def pts(self, v): + if v is None: + self.ptr.pts = lib.AV_NOPTS_VALUE + else: + self.ptr.pts = v - property dts: + @property + def dts(self): """ The decoding timestamp in :attr:`time_base` units for this packet. :type: int """ - def __get__(self): - if self.ptr.dts != lib.AV_NOPTS_VALUE: - return self.ptr.dts + if self.ptr.dts != lib.AV_NOPTS_VALUE: + return self.ptr.dts - def __set__(self, v): - if v is None: - self.ptr.dts = lib.AV_NOPTS_VALUE - else: - self.ptr.dts = v + @dts.setter + def dts(self, v): + if v is None: + self.ptr.dts = lib.AV_NOPTS_VALUE + else: + self.ptr.dts = v - property pos: + @property + def pos(self): """ The byte position of this packet within the :class:`.Stream`. @@ -168,20 +153,20 @@ cdef class Packet(Buffer): :type: int """ - def __get__(self): - if self.ptr.pos != -1: - return self.ptr.pos + if self.ptr.pos != -1: + return self.ptr.pos - property size: + @property + def size(self): """ The size in bytes of this packet's data. :type: int """ - def __get__(self): - return self.ptr.size + return self.ptr.size - property duration: + @property + def duration(self): """ The duration in :attr:`time_base` units for this packet. @@ -189,12 +174,40 @@ cdef class Packet(Buffer): :type: int """ - def __get__(self): - if self.ptr.duration != lib.AV_NOPTS_VALUE: - return self.ptr.duration + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration + + @property + def is_keyframe(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) + + @is_keyframe.setter + def is_keyframe(self, v): + if v: + self.ptr.flags |= lib.AV_PKT_FLAG_KEY + else: + self.ptr.flags &= ~(lib.AV_PKT_FLAG_KEY) + + @property + def is_corrupt(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) + + @is_corrupt.setter + def is_corrupt(self, v): + if v: + self.ptr.flags |= lib.AV_PKT_FLAG_CORRUPT + else: + self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) + + @property + def is_discard(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISCARD) + + @property + def is_trusted(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_TRUSTED) - property is_keyframe: - def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) + @property + def is_disposable(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISPOSABLE) - property is_corrupt: - def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) diff --git a/av/plane.pyi b/av/plane.pyi new file mode 100644 index 000000000..99594d11a --- /dev/null +++ b/av/plane.pyi @@ -0,0 +1,8 @@ +from .buffer import Buffer +from .frame import Frame + +class Plane(Buffer): + frame: Frame + index: int + + def __init__(self, frame: Frame, index: int) -> None: ... diff --git a/av/plane.pyx b/av/plane.pyx index e3ec291a6..c733b20a7 100644 --- a/av/plane.pyx +++ b/av/plane.pyx @@ -11,11 +11,9 @@ cdef class Plane(Buffer): self.index = index def __repr__(self): - return '' % ( - self.__class__.__name__, - self.buffer_size, - self.buffer_ptr, - id(self), + return ( + f"" ) cdef void* _buffer_ptr(self): diff --git a/av/py.typed b/av/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/av/sidedata/motionvectors.pyi b/av/sidedata/motionvectors.pyi new file mode 100644 index 000000000..eb514eb70 --- /dev/null +++ b/av/sidedata/motionvectors.pyi @@ -0,0 +1,27 @@ +from typing import Any, Sequence, overload + +import numpy as np + +from .sidedata import SideData + +class MotionVectors(SideData, Sequence[MotionVector]): + @overload + def __getitem__(self, index: int): ... + @overload + def __getitem__(self, index: slice): ... + @overload + def __getitem__(self, index: int | slice): ... + def __len__(self) -> int: ... + def to_ndarray(self) -> np.ndarray[Any, Any]: ... + +class MotionVector: + source: int + w: int + h: int + src_x: int + src_y: int + dst_x: int + dst_y: int + motion_x: int + motion_y: int + motion_scale: int diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx index f1e12d56f..b0b0b705f 100644 --- a/av/sidedata/motionvectors.pyx +++ b/av/sidedata/motionvectors.pyx @@ -7,14 +7,13 @@ cdef object _cinit_bypass_sentinel = object() # Cython doesn't let us inherit from the abstract Sequence, so we will subclass # it later. cdef class _MotionVectors(SideData): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._vectors = {} self._len = self.ptr.size // sizeof(lib.AVMotionVector) def __repr__(self): - return f'self.ptr.data:0x}' + return f"self.ptr.data:0x}" def __getitem__(self, int index): @@ -35,17 +34,17 @@ cdef class _MotionVectors(SideData): def to_ndarray(self): import numpy as np return np.frombuffer(self, dtype=np.dtype([ - ('source', 'int32'), - ('w', 'uint8'), - ('h', 'uint8'), - ('src_x', 'int16'), - ('src_y', 'int16'), - ('dst_x', 'int16'), - ('dst_y', 'int16'), - ('flags', 'uint64'), - ('motion_x', 'int32'), - ('motion_y', 'int32'), - ('motion_scale', 'uint16'), + ("source", "int32"), + ("w", "uint8"), + ("h", "uint8"), + ("src_x", "int16"), + ("src_y", "int16"), + ("dst_x", "int16"), + ("dst_y", "int16"), + ("flags", "uint64"), + ("motion_x", "int32"), + ("motion_y", "int32"), + ("motion_scale", "uint16"), ], align=True)) @@ -54,16 +53,15 @@ class MotionVectors(_MotionVectors, Sequence): cdef class MotionVector: - def __init__(self, sentinel, _MotionVectors parent, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError('cannot manually instatiate MotionVector') + raise RuntimeError("cannot manually instatiate MotionVector") self.parent = parent cdef lib.AVMotionVector *base = parent.ptr.data self.ptr = base + index def __repr__(self): - return f'' + return f"" @property def source(self): diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi new file mode 100644 index 000000000..ac28f0dee --- /dev/null +++ b/av/sidedata/sidedata.pyi @@ -0,0 +1,40 @@ +from collections.abc import Mapping +from typing import Iterator, Sequence, overload + +from av.buffer import Buffer +from av.enum import EnumItem +from av.frame import Frame + +class Type(EnumItem): + PANSCAN: int + A53_CC: int + STEREO3D: int + MATRIXENCODING: int + DOWNMIX_INFO: int + REPLAYGAIN: int + DISPLAYMATRIX: int + AFD: int + MOTION_VECTORS: int + SKIP_SAMPLES: int + AUDIO_SERVICE_TYPE: int + MASTERING_DISPLAY_METADATA: int + GOP_TIMECODE: int + SPHERICAL: int + CONTENT_LIGHT_LEVEL: int + ICC_PROFILE: int + SEI_UNREGISTERED: int + +class SideData(Buffer): + type: Type + DISPLAYMATRIX: int + +class SideDataContainer(Mapping): + frame: Frame + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[SideData]: ... + @overload + def __getitem__(self, key: int) -> SideData: ... + @overload + def __getitem__(self, key: slice) -> Sequence[SideData]: ... + @overload + def __getitem__(self, key: int | slice) -> SideData | Sequence[SideData]: ... diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 17bb869cc..05ed00219 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -8,35 +8,28 @@ from av.sidedata.motionvectors import MotionVectors cdef object _cinit_bypass_sentinel = object() -Type = define_enum('Type', __name__, ( - ('PANSCAN', lib.AV_FRAME_DATA_PANSCAN), - ('A53_CC', lib.AV_FRAME_DATA_A53_CC), - ('STEREO3D', lib.AV_FRAME_DATA_STEREO3D), - ('MATRIXENCODING', lib.AV_FRAME_DATA_MATRIXENCODING), - ('DOWNMIX_INFO', lib.AV_FRAME_DATA_DOWNMIX_INFO), - ('REPLAYGAIN', lib.AV_FRAME_DATA_REPLAYGAIN), - ('DISPLAYMATRIX', lib.AV_FRAME_DATA_DISPLAYMATRIX), - ('AFD', lib.AV_FRAME_DATA_AFD), - ('MOTION_VECTORS', lib.AV_FRAME_DATA_MOTION_VECTORS), - ('SKIP_SAMPLES', lib.AV_FRAME_DATA_SKIP_SAMPLES), - ('AUDIO_SERVICE_TYPE', lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE), - ('MASTERING_DISPLAY_METADATA', lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA), - ('GOP_TIMECODE', lib.AV_FRAME_DATA_GOP_TIMECODE), - ('SPHERICAL', lib.AV_FRAME_DATA_SPHERICAL), - ('CONTENT_LIGHT_LEVEL', lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), - ('ICC_PROFILE', lib.AV_FRAME_DATA_ICC_PROFILE), - # SEI_UNREGISTERED available since version 56.54.100 of libavutil (FFmpeg >= 4.4) - ('SEI_UNREGISTERED', lib.AV_FRAME_DATA_SEI_UNREGISTERED) if lib.AV_FRAME_DATA_SEI_UNREGISTERED != -1 else None, - - # These are deprecated. See https://github.com/PyAV-Org/PyAV/issues/607 - # ('QP_TABLE_PROPERTIES', lib.AV_FRAME_DATA_QP_TABLE_PROPERTIES), - # ('QP_TABLE_DATA', lib.AV_FRAME_DATA_QP_TABLE_DATA), - +Type = define_enum("Type", __name__, ( + ("PANSCAN", lib.AV_FRAME_DATA_PANSCAN), + ("A53_CC", lib.AV_FRAME_DATA_A53_CC), + ("STEREO3D", lib.AV_FRAME_DATA_STEREO3D), + ("MATRIXENCODING", lib.AV_FRAME_DATA_MATRIXENCODING), + ("DOWNMIX_INFO", lib.AV_FRAME_DATA_DOWNMIX_INFO), + ("REPLAYGAIN", lib.AV_FRAME_DATA_REPLAYGAIN), + ("DISPLAYMATRIX", lib.AV_FRAME_DATA_DISPLAYMATRIX), + ("AFD", lib.AV_FRAME_DATA_AFD), + ("MOTION_VECTORS", lib.AV_FRAME_DATA_MOTION_VECTORS), + ("SKIP_SAMPLES", lib.AV_FRAME_DATA_SKIP_SAMPLES), + ("AUDIO_SERVICE_TYPE", lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE), + ("MASTERING_DISPLAY_METADATA", lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA), + ("GOP_TIMECODE", lib.AV_FRAME_DATA_GOP_TIMECODE), + ("SPHERICAL", lib.AV_FRAME_DATA_SPHERICAL), + ("CONTENT_LIGHT_LEVEL", lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), + ("ICC_PROFILE", lib.AV_FRAME_DATA_ICC_PROFILE), + ("SEI_UNREGISTERED", lib.AV_FRAME_DATA_SEI_UNREGISTERED) if lib.AV_FRAME_DATA_SEI_UNREGISTERED != -1 else None, )) cdef SideData wrap_side_data(Frame frame, int index): - cdef lib.AVFrameSideDataType type_ = frame.ptr.side_data[index].type if type_ == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) @@ -45,10 +38,9 @@ cdef SideData wrap_side_data(Frame frame, int index): cdef class SideData(Buffer): - def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError('cannot manually instatiate SideData') + raise RuntimeError("cannot manually instatiate SideData") self.frame = frame self.ptr = frame.ptr.side_data[index] self.metadata = wrap_dictionary(self.ptr.metadata) @@ -63,7 +55,7 @@ cdef class SideData(Buffer): return False def __repr__(self): - return f'self.ptr.data:0x}>' + return f"self.ptr.data:0x}>" @property def type(self): @@ -71,9 +63,7 @@ cdef class SideData(Buffer): cdef class _SideDataContainer: - def __init__(self, Frame frame): - self.frame = frame self._by_index = [] self._by_type = {} @@ -92,7 +82,6 @@ cdef class _SideDataContainer: return iter(self._by_index) def __getitem__(self, key): - if isinstance(key, int): return self._by_index[key] diff --git a/av/stream.pxd b/av/stream.pxd index c847f641e..8ebda5704 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -12,6 +12,8 @@ cdef class Stream: # Stream attributes. cdef readonly Container container cdef readonly dict metadata + cdef readonly int nb_side_data + cdef readonly dict side_data # CodecContext attributes. cdef readonly CodecContext codec_context @@ -19,6 +21,7 @@ cdef class Stream: # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) + cdef _get_side_data(self, lib.AVStream *stream) cdef _set_time_base(self, value) cdef _set_id(self, value) diff --git a/av/stream.pyi b/av/stream.pyi new file mode 100644 index 000000000..ffe4f8722 --- /dev/null +++ b/av/stream.pyi @@ -0,0 +1,35 @@ +from fractions import Fraction +from typing import Literal + +from .codec import CodecContext +from .container import Container +from .enum import EnumItem +from .frame import Frame +from .packet import Packet + +class SideData(EnumItem): + DISPLAYMATRIX: int + +class Stream: + name: str | None + thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] + + container: Container + codec_context: CodecContext + metadata: dict[str, str] + id: int + profile: str + nb_side_data: int + side_data: dict[str, str] + index: int + time_base: Fraction | None + average_rate: Fraction | None + base_rate: Fraction | None + guessed_rate: Fraction | None + start_time: int | None + duration: int | None + frames: int + language: str | None + type: Literal["video", "audio", "data", "subtitle", "attachment"] + + def encode(self, frame: Frame | None = None) -> list[Packet]: ... diff --git a/av/stream.pyx b/av/stream.pyx index f9b6d7ec5..0ed3e50df 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,14 +1,16 @@ import warnings cimport libav as lib +from libc.stdint cimport int32_t +from av.enum cimport define_enum from av.error cimport err_check from av.packet cimport Packet from av.utils cimport ( avdict_to_dict, avrational_to_fraction, dict_to_avdict, - to_avrational + to_avrational, ) from av.deprecation import AVDeprecationWarning @@ -17,6 +19,12 @@ from av.deprecation import AVDeprecationWarning cdef object _cinit_bypass_sentinel = object() +# If necessary more can be added from +# https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 +SideData = define_enum("SideData", __name__, ( + ("DISPLAYMATRIX", lib.AV_PKT_DATA_DISPLAYMATRIX, "Display Matrix"), +)) + cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. @@ -69,7 +77,7 @@ cdef class Stream: def __cinit__(self, name): if name is _cinit_bypass_sentinel: return - raise RuntimeError('cannot manually instantiate Stream') + raise RuntimeError("cannot manually instantiate Stream") cdef _init(self, Container container, lib.AVStream *stream, CodecContext codec_context): self.container = container @@ -79,6 +87,8 @@ cdef class Stream: if self.codec_context: self.codec_context.stream_index = stream.index + self.nb_side_data, self.side_data = self._get_side_data(stream) + self.metadata = avdict_to_dict( stream.metadata, encoding=self.container.metadata_encoding, @@ -86,12 +96,9 @@ cdef class Stream: ) def __repr__(self): - return '' % ( - self.__class__.__name__, - self.index, - self.type or '', - self.name or '', - id(self), + return ( + f"'}/" + f"{self.name or ''} at 0x{id(self):x}>" ) def __getattr__(self, name): @@ -99,10 +106,15 @@ cdef class Stream: # See: https://github.com/PyAV-Org/PyAV/issues/1005 if self.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO and name in ("framerate", "rate"): warnings.warn( - "VideoStream.%s is deprecated as it is not always set; please use VideoStream.average_rate." % name, + f"VideoStream.{name} is deprecated as it is not always set; please use VideoStream.average_rate.", AVDeprecationWarning ) + if name == "side_data": + return self.side_data + elif name == "nb_side_data": + return self.nb_side_data + # Convenience getter for codec context properties. if self.codec_context is not None: return getattr(self.codec_context, name) @@ -153,29 +165,29 @@ cdef class Stream: packet.ptr.stream_index = self.ptr.index return packets - def decode(self, packet=None): - """ - Decode a :class:`.Packet` and return a list of :class:`.AudioFrame` - or :class:`.VideoFrame`. + cdef _get_side_data(self, lib.AVStream *stream): + # Get DISPLAYMATRIX SideData from a lib.AVStream object. + # Returns: tuple[int, dict[str, Any]] - :return: :class:`list` of :class:`.Frame` subclasses. - - .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.decode`. - """ - if self.codec_context is None: - raise RuntimeError("Stream.decode requires a valid CodecContext") + nb_side_data = stream.nb_side_data + side_data = {} + + for i in range(nb_side_data): + # Based on: https://www.ffmpeg.org/doxygen/trunk/dump_8c_source.html#l00430 + if stream.side_data[i].type == lib.AV_PKT_DATA_DISPLAYMATRIX: + side_data["DISPLAYMATRIX"] = lib.av_display_rotation_get(stream.side_data[i].data) - return self.codec_context.decode(packet) + return nb_side_data, side_data - property id: + @property + def id(self): """ The format-specific ID of this stream. :type: int """ - def __get__(self): - return self.ptr.id + return self.ptr.id cdef _set_id(self, value): """ @@ -186,35 +198,37 @@ cdef class Stream: else: self.ptr.id = value - property profile: + @property + def profile(self): """ The profile of this stream. :type: str """ - def __get__(self): - if self.codec_context: - return self.codec_context.profile - else: - return None + if self.codec_context: + return self.codec_context.profile + else: + return None - property index: + @property + def index(self): """ The index of this stream in its :class:`.Container`. :type: int """ - def __get__(self): return self.ptr.index + return self.ptr.index + - property time_base: + @property + def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. :type: :class:`~fractions.Fraction` or ``None`` """ - def __get__(self): - return avrational_to_fraction(&self.ptr.time_base) + return avrational_to_fraction(&self.ptr.time_base) cdef _set_time_base(self, value): """ @@ -222,7 +236,8 @@ cdef class Stream: """ to_avrational(value, &self.ptr.time_base) - property average_rate: + @property + def average_rate(self): """ The average frame rate of this video stream. @@ -233,10 +248,10 @@ cdef class Stream: """ - def __get__(self): - return avrational_to_fraction(&self.ptr.avg_frame_rate) + return avrational_to_fraction(&self.ptr.avg_frame_rate) - property base_rate: + @property + def base_rate(self): """ The base frame rate of this stream. @@ -247,10 +262,10 @@ cdef class Stream: :type: :class:`~fractions.Fraction` or ``None`` """ - def __get__(self): - return avrational_to_fraction(&self.ptr.r_frame_rate) + return avrational_to_fraction(&self.ptr.r_frame_rate) - property guessed_rate: + @property + def guessed_rate(self): """The guessed frame rate of this stream. This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple @@ -259,34 +274,34 @@ cdef class Stream: :type: :class:`~fractions.Fraction` or ``None`` """ - def __get__(self): - # The two NULL arguments aren't used in FFmpeg >= 4.0 - cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) - return avrational_to_fraction(&val) + # The two NULL arguments aren't used in FFmpeg >= 4.0 + cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) + return avrational_to_fraction(&val) - property start_time: + @property + def start_time(self): """ The presentation timestamp in :attr:`time_base` units of the first frame in this stream. :type: :class:`int` or ``None`` """ - def __get__(self): - if self.ptr.start_time != lib.AV_NOPTS_VALUE: - return self.ptr.start_time + if self.ptr.start_time != lib.AV_NOPTS_VALUE: + return self.ptr.start_time - property duration: + @property + def duration(self): """ The duration of this stream in :attr:`time_base` units. :type: :class:`int` or ``None`` """ - def __get__(self): - if self.ptr.duration != lib.AV_NOPTS_VALUE: - return self.ptr.duration + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration - property frames: + @property + def frames(self): """ The number of frames this stream contains. @@ -294,17 +309,16 @@ cdef class Stream: :type: :class:`int` """ - def __get__(self): - return self.ptr.nb_frames + return self.ptr.nb_frames - property language: + @property + def language(self): """ The language of the stream. :type: :class:`str` or ``None`` """ - def __get__(self): - return self.metadata.get('language') + return self.metadata.get('language') @property def type(self): diff --git a/av/subtitles/codeccontext.pyi b/av/subtitles/codeccontext.pyi new file mode 100644 index 000000000..0762c19f0 --- /dev/null +++ b/av/subtitles/codeccontext.pyi @@ -0,0 +1,6 @@ +from typing import Literal + +from av.codec.context import CodecContext + +class SubtitleCodecContext(CodecContext): + type: Literal["subtitle"] diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index c3f433abe..227add919 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -6,7 +6,6 @@ from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet cdef class SubtitleCodecContext(CodecContext): - cdef _send_packet_and_recv(self, Packet packet): cdef SubtitleProxy proxy = SubtitleProxy() diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi new file mode 100644 index 000000000..38f4ae6cb --- /dev/null +++ b/av/subtitles/stream.pyi @@ -0,0 +1,3 @@ +from av.stream import Stream + +class SubtitleStream(Stream): ... diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index aea5c57e2..8da6043f0 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,3 +1,2 @@ - cdef class SubtitleStream(Stream): pass diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi new file mode 100644 index 000000000..0363c2399 --- /dev/null +++ b/av/subtitles/subtitle.pyi @@ -0,0 +1,36 @@ +from typing import Iterator, Literal + +class SubtitleSet: + format: int + start_display_time: int + end_display_time: int + pts: int + + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[Subtitle]: ... + def __getitem__(self, i: int) -> Subtitle: ... + +class Subtitle: + type: Literal[b"none", b"bitmap", b"text", b"ass"] + +class BitmapSubtitle(Subtitle): + type: Literal[b"bitmap"] + x: int + y: int + width: int + height: int + nb_colors: int + planes: tuple[BitmapSubtitlePlane, ...] + +class BitmapSubtitlePlane: + subtitle: BitmapSubtitle + index: int + buffer_size: int + +class TextSubtitle(Subtitle): + type: Literal[b"text"] + text: str + +class AssSubtitle(Subtitle): + type: Literal[b"ass"] + ass: str diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 1f0e4319a..5b2fc8a4a 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -7,27 +7,22 @@ cdef class SubtitleProxy: cdef class SubtitleSet: - def __cinit__(self, SubtitleProxy proxy): self.proxy = proxy cdef int i self.rects = tuple(build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)) def __repr__(self): - return '<%s.%s at 0x%x>' % ( - self.__class__.__module__, - self.__class__.__name__, - id(self), - ) + return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" - property format: - def __get__(self): return self.proxy.struct.format - property start_display_time: - def __get__(self): return self.proxy.struct.start_display_time - property end_display_time: - def __get__(self): return self.proxy.struct.end_display_time - property pts: - def __get__(self): return self.proxy.struct.pts + @property + def format(self): return self.proxy.struct.format + @property + def start_display_time(self): return self.proxy.struct.start_display_time + @property + def end_display_time(self): return self.proxy.struct.end_display_time + @property + def pts(self): return self.proxy.struct.pts def __len__(self): return len(self.rects) @@ -48,7 +43,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): """ if index < 0 or index >= subtitle.proxy.struct.num_rects: - raise ValueError('subtitle rect index out of range') + raise ValueError("subtitle rect index out of range") cdef lib.AVSubtitleRect *ptr = subtitle.proxy.struct.rects[index] if ptr.type == lib.SUBTITLE_NONE: @@ -60,38 +55,32 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): elif ptr.type == lib.SUBTITLE_ASS: return AssSubtitle(subtitle, index) else: - raise ValueError('unknown subtitle type %r' % ptr.type) + raise ValueError("unknown subtitle type %r" % ptr.type) cdef class Subtitle: - def __cinit__(self, SubtitleSet subtitle, int index): if index < 0 or index >= subtitle.proxy.struct.num_rects: - raise ValueError('subtitle rect index out of range') + raise ValueError("subtitle rect index out of range") self.proxy = subtitle.proxy self.ptr = self.proxy.struct.rects[index] if self.ptr.type == lib.SUBTITLE_NONE: - self.type = b'none' + self.type = b"none" elif self.ptr.type == lib.SUBTITLE_BITMAP: - self.type = b'bitmap' + self.type = b"bitmap" elif self.ptr.type == lib.SUBTITLE_TEXT: - self.type = b'text' + self.type = b"text" elif self.ptr.type == lib.SUBTITLE_ASS: - self.type = b'ass' + self.type = b"ass" else: - raise ValueError('unknown subtitle type %r' % self.ptr.type) + raise ValueError(f"unknown subtitle type {self.ptr.type!r}") def __repr__(self): - return '<%s.%s at 0x%x>' % ( - self.__class__.__module__, - self.__class__.__name__, - id(self), - ) + return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" cdef class BitmapSubtitle(Subtitle): - def __cinit__(self, SubtitleSet subtitle, int index): self.planes = tuple( BitmapSubtitlePlane(self, i) @@ -100,26 +89,21 @@ cdef class BitmapSubtitle(Subtitle): ) def __repr__(self): - return '<%s.%s %dx%d at %d,%d; at 0x%x>' % ( - self.__class__.__module__, - self.__class__.__name__, - self.width, - self.height, - self.x, - self.y, - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"{self.width}x{self.height} at {self.x},{self.y}; at 0x{id(self):x}>" ) - property x: - def __get__(self): return self.ptr.x - property y: - def __get__(self): return self.ptr.y - property width: - def __get__(self): return self.ptr.w - property height: - def __get__(self): return self.ptr.h - property nb_colors: - def __get__(self): return self.ptr.nb_colors + @property + def x(self): return self.ptr.x + @property + def y(self): return self.ptr.y + @property + def width(self): return self.ptr.w + @property + def height(self): return self.ptr.h + @property + def nb_colors(self): return self.ptr.nb_colors def __len__(self): return len(self.planes) @@ -132,13 +116,11 @@ cdef class BitmapSubtitle(Subtitle): cdef class BitmapSubtitlePlane: - def __cinit__(self, BitmapSubtitle subtitle, int index): - if index >= 4: - raise ValueError('BitmapSubtitles have only 4 planes') + raise ValueError("BitmapSubtitles have only 4 planes") if not subtitle.ptr.linesize[index]: - raise ValueError('plane does not exist') + raise ValueError("plane does not exist") self.subtitle = subtitle self.index = index @@ -146,34 +128,29 @@ cdef class BitmapSubtitlePlane: self._buffer = subtitle.ptr.data[index] # New-style buffer support. - def __getbuffer__(self, Py_buffer *view, int flags): PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) cdef class TextSubtitle(Subtitle): - def __repr__(self): - return '<%s.%s %r at 0x%x>' % ( - self.__class__.__module__, - self.__class__.__name__, - self.text, - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"{self.text!r} at 0x{id(self):x}>" ) - property text: - def __get__(self): return self.ptr.text + @property + def text(self): + return self.ptr.text cdef class AssSubtitle(Subtitle): - def __repr__(self): - return '<%s.%s %r at 0x%x>' % ( - self.__class__.__module__, - self.__class__.__name__, - self.ass, - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"{self.ass!r} at 0x{id(self):x}>" ) - property ass: - def __get__(self): return self.ptr.ass + @property + def ass(self): + return self.ptr.ass diff --git a/av/utils.pxd b/av/utils.pxd index bc5d56927..125272c5c 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport uint64_t cimport libav as lib +from libc.stdint cimport uint64_t cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors) diff --git a/av/utils.pyx b/av/utils.pyx index 1b25a38fe..cc01925de 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -6,7 +6,6 @@ cimport libav as lib from av.error cimport err_check - # === DICTIONARIES === # ==================== @@ -30,8 +29,14 @@ cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors): cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors): lib.av_dict_free(dst) for key, value in src.items(): - err_check(lib.av_dict_set(dst, _encode(key, encoding, errors), - _encode(value, encoding, errors), 0)) + err_check( + lib.av_dict_set( + dst, + _encode(key, encoding, errors), + _encode(value, encoding, errors), + 0 + ) + ) # === FRACTIONS === @@ -43,7 +48,6 @@ cdef object avrational_to_fraction(const lib.AVRational *input): cdef object to_avrational(object value, lib.AVRational *input): - if value is None: input.num = 0 input.den = 1 diff --git a/av/video/__init__.pyi b/av/video/__init__.pyi new file mode 100644 index 000000000..4a25d8837 --- /dev/null +++ b/av/video/__init__.pyi @@ -0,0 +1,2 @@ +from .frame import VideoFrame +from .stream import VideoStream diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi new file mode 100644 index 000000000..a749ad3f9 --- /dev/null +++ b/av/video/codeccontext.pyi @@ -0,0 +1,30 @@ +from fractions import Fraction +from typing import Iterator, Literal + +from av.codec.context import CodecContext +from av.packet import Packet + +from .frame import VideoFrame + +class VideoCodecContext(CodecContext): + width: int + height: int + bits_per_codec_sample: int + pix_fmt: str + framerate: Fraction + rate: Fraction + gop_size: int + sample_aspect_ratio: Fraction + display_aspect_ratio: Fraction + has_b_frames: bool + coded_width: int + coded_height: int + color_range: int + color_primaries: int + color_trc: int + colorspace: int + + type: Literal["video"] + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 891f5ccaf..700c91279 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -1,5 +1,7 @@ -from libc.stdint cimport int64_t +import warnings + cimport libav as lib +from libc.stdint cimport int64_t from av.codec.context cimport CodecContext from av.frame cimport Frame @@ -9,9 +11,10 @@ from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.frame cimport VideoFrame, alloc_video_frame from av.video.reformatter cimport VideoReformatter +from av.deprecation import AVDeprecationWarning -cdef class VideoCodecContext(CodecContext): +cdef class VideoCodecContext(CodecContext): def __cinit__(self, *args, **kwargs): self.last_w = 0 self.last_h = 0 @@ -26,7 +29,6 @@ cdef class VideoCodecContext(CodecContext): self.ptr.time_base.den = self.ptr.framerate.num or lib.AV_TIME_BASE cdef _prepare_frames_for_encode(self, Frame input): - if not input: return [None] @@ -66,106 +68,218 @@ cdef class VideoCodecContext(CodecContext): cdef _build_format(self): self._format = get_video_format(self.ptr.pix_fmt, self.ptr.width, self.ptr.height) - property format: - def __get__(self): - return self._format - - def __set__(self, VideoFormat format): - self.ptr.pix_fmt = format.pix_fmt - self.ptr.width = format.width - self.ptr.height = format.height - self._build_format() # Kinda wasteful. + @property + def format(self): + return self._format - property width: - def __get__(self): - return self.ptr.width + @format.setter + def format(self, VideoFormat format): + self.ptr.pix_fmt = format.pix_fmt + self.ptr.width = format.width + self.ptr.height = format.height + self._build_format() # Kinda wasteful. - def __set__(self, unsigned int value): - self.ptr.width = value - self._build_format() + @property + def width(self): + return self.ptr.width - property height: - def __get__(self): - return self.ptr.height + @width.setter + def width(self, unsigned int value): + self.ptr.width = value + self._build_format() - def __set__(self, unsigned int value): - self.ptr.height = value - self._build_format() + @property + def height(self): + return self.ptr.height - property bits_per_coded_sample: - def __get__(self): - return self.ptr.bits_per_coded_sample + @height.setter + def height(self, unsigned int value): + self.ptr.height = value + self._build_format() - def __set__(self, unsigned int value): - self.ptr.bits_per_coded_sample = value - self._build_format() + @property + def bits_per_coded_sample(self): + """ + The number of bits per sample in the codedwords, basically the bitrate per sample. + It is mandatory for this to be set for some formats to decode them. + + :type: int + """ + return self.ptr.bits_per_coded_sample + + @bits_per_coded_sample.setter + def bits_per_coded_sample(self, int value): + self.ptr.bits_per_coded_sample = value + self._build_format() - property pix_fmt: + @property + def pix_fmt(self): """ The pixel format's name. :type: str """ - def __get__(self): - return self._format.name + return self._format.name - def __set__(self, value): - self.ptr.pix_fmt = get_pix_fmt(value) - self._build_format() + @pix_fmt.setter + def pix_fmt(self, value): + self.ptr.pix_fmt = get_pix_fmt(value) + self._build_format() - property framerate: + @property + def framerate(self): """ The frame rate, in frames per second. :type: fractions.Fraction """ - def __get__(self): - return avrational_to_fraction(&self.ptr.framerate) + return avrational_to_fraction(&self.ptr.framerate) - def __set__(self, value): - to_avrational(value, &self.ptr.framerate) + @framerate.setter + def framerate(self, value): + to_avrational(value, &self.ptr.framerate) - property rate: + @property + def rate(self): """Another name for :attr:`framerate`.""" - def __get__(self): - return self.framerate + return self.framerate - def __set__(self, value): - self.framerate = value + @rate.setter + def rate(self, value): + self.framerate = value - property gop_size: - def __get__(self): - return self.ptr.gop_size + @property + def gop_size(self): + """ + Sets the number of frames between keyframes. Used only for encoding. + + :type: int + """ + if self.is_decoder: + warnings.warn( + "Using VideoCodecContext.gop_size for decoders is deprecated.", + AVDeprecationWarning + ) + return self.ptr.gop_size + + @gop_size.setter + def gop_size(self, int value): + if self.is_decoder: + warnings.warn( + "Using VideoCodecContext.gop_size for decoders is deprecated.", + AVDeprecationWarning + ) + self.ptr.gop_size = value - def __set__(self, int value): - self.ptr.gop_size = value + @property + def sample_aspect_ratio(self): + return avrational_to_fraction(&self.ptr.sample_aspect_ratio) - property sample_aspect_ratio: - def __get__(self): - return avrational_to_fraction(&self.ptr.sample_aspect_ratio) + @sample_aspect_ratio.setter + def sample_aspect_ratio(self, value): + to_avrational(value, &self.ptr.sample_aspect_ratio) - def __set__(self, value): - to_avrational(value, &self.ptr.sample_aspect_ratio) + @property + def display_aspect_ratio(self): + cdef lib.AVRational dar - property display_aspect_ratio: - def __get__(self): - cdef lib.AVRational dar + lib.av_reduce( + &dar.num, &dar.den, + self.ptr.width * self.ptr.sample_aspect_ratio.num, + self.ptr.height * self.ptr.sample_aspect_ratio.den, 1024*1024) - lib.av_reduce( - &dar.num, &dar.den, - self.ptr.width * self.ptr.sample_aspect_ratio.num, - self.ptr.height * self.ptr.sample_aspect_ratio.den, 1024*1024) + return avrational_to_fraction(&dar) - return avrational_to_fraction(&dar) + @property + def has_b_frames(self): + """ + :type: bool + """ + return bool(self.ptr.has_b_frames) - property has_b_frames: - def __get__(self): - return bool(self.ptr.has_b_frames) + @property + def coded_width(self): + """ + :type: int + """ + return self.ptr.coded_width + + @property + def coded_height(self): + """ + :type: int + """ + return self.ptr.coded_height + + @property + def color_range(self): + """ + Describes the signal range of the colorspace. - property coded_width: - def __get__(self): - return self.ptr.coded_width + Wraps :ffmpeg:`AVFrame.color_range`. + + :type: int + """ + return self.ptr.color_range + + @color_range.setter + def color_range(self, value): + self.ptr.color_range = value + + @property + def color_primaries(self): + """ + Describes the RGB/XYZ matrix of the colorspace. + + Wraps :ffmpeg:`AVFrame.color_primaries`. + + :type: int + """ + return self.ptr.color_primaries + + @color_primaries.setter + def color_primaries(self, value): + self.ptr.color_primaries = value + + @property + def color_trc(self): + """ + Describes the linearization function (a.k.a. transformation characteristics) of the colorspace. + + Wraps :ffmpeg:`AVFrame.color_trc`. + + :type: int + """ + return self.ptr.color_trc + + @color_trc.setter + def color_trc(self, value): + self.ptr.color_trc = value + + @property + def colorspace(self): + """ + Describes the YUV/RGB transformation matrix of the colorspace. + + Wraps :ffmpeg:`AVFrame.colorspace`. + + :type: int + """ + return self.ptr.colorspace + + @colorspace.setter + def colorspace(self, value): + self.ptr.colorspace = value + + @property + def max_b_frames(self): + """ + The maximum run of consecutive B frames when encoding a video. + + :type: int + """ + return self.ptr.max_b_frames - property coded_height: - def __get__(self): - return self.ptr.coded_height + @max_b_frames.setter + def max_b_frames(self, value): + self.ptr.max_b_frames = value diff --git a/av/video/format.pyi b/av/video/format.pyi new file mode 100644 index 000000000..16739a0e3 --- /dev/null +++ b/av/video/format.pyi @@ -0,0 +1,24 @@ +class VideoFormat: + name: str + bits_per_pixel: int + padded_bits_per_pixel: int + is_big_endian: bool + has_palette: bool + is_bit_stream: bool + is_planar: bool + is_rgb: bool + + def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... + def chroma_width(self, luma_width: int = 0) -> int: ... + def chroma_height(self, luma_height: int = 0) -> int: ... + +class VideoFormatComponent: + plane: int + bits: int + is_alpha: bool + is_luma: bool + is_chroma: bool + width: int + height: int + + def __init__(self, format: VideoFormat, index: int) -> None: ... diff --git a/av/video/format.pyx b/av/video/format.pyx index 602ec5275..6ae66c3a2 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -4,6 +4,7 @@ cdef object _cinit_bypass_sentinel = object() cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height): if c_format == lib.AV_PIX_FMT_NONE: return None + cdef VideoFormat format = VideoFormat.__new__(VideoFormat, _cinit_bypass_sentinel) format._init(c_format, width, height) return format @@ -14,7 +15,7 @@ cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE: cdef lib.AVPixelFormat pix_fmt = lib.av_get_pix_fmt(name) if pix_fmt == lib.AV_PIX_FMT_NONE: - raise ValueError('not a pixel format: %r' % name) + raise ValueError("not a pixel format: %r" % name) return pix_fmt @@ -29,7 +30,6 @@ cdef class VideoFormat: """ def __cinit__(self, name, width=0, height=0): - if name is _cinit_bypass_sentinel: return @@ -54,46 +54,57 @@ cdef class VideoFormat: def __repr__(self): if self.width or self.height: - return '' % (self.__class__.__name__, self.name, self.width, self.height) + return f"" else: - return '' % (self.__class__.__name__, self.name) + return f"" def __int__(self): return int(self.pix_fmt) - property name: + @property + def name(self): """Canonical name of the pixel format.""" - def __get__(self): - return self.ptr.name + return self.ptr.name - property bits_per_pixel: - def __get__(self): return lib.av_get_bits_per_pixel(self.ptr) + @property + def bits_per_pixel(self): + return lib.av_get_bits_per_pixel(self.ptr) - property padded_bits_per_pixel: - def __get__(self): return lib.av_get_padded_bits_per_pixel(self.ptr) + @property + def padded_bits_per_pixel(self): return lib.av_get_padded_bits_per_pixel(self.ptr) - property is_big_endian: + @property + def is_big_endian(self): """Pixel format is big-endian.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BE) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BE) + - property has_palette: + @property + def has_palette(self): """Pixel format has a palette in data[1], values are indexes in this palette.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PAL) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PAL) - property is_bit_stream: + + @property + def is_bit_stream(self): """All values of a component are bit-wise packed end to end.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BITSTREAM) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BITSTREAM) + # Skipping PIX_FMT_HWACCEL # """Pixel format is an HW accelerated format.""" - property is_planar: + @property + def is_planar(self): """At least one pixel component is not in the first data plane.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PLANAR) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PLANAR) - property is_rgb: + + @property + def is_rgb(self): """The pixel format contains RGB-like data (as opposed to YUV/grayscale).""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) + cpdef chroma_width(self, int luma_width=0): """chroma_width(luma_width=0) @@ -119,59 +130,58 @@ cdef class VideoFormat: cdef class VideoFormatComponent: - def __cinit__(self, VideoFormat format, size_t index): self.format = format self.index = index self.ptr = &format.ptr.comp[index] - property plane: + @property + def plane(self): """The index of the plane which contains this component.""" - def __get__(self): - return self.ptr.plane + return self.ptr.plane - property bits: + @property + def bits(self): """Number of bits in the component.""" - def __get__(self): - return self.ptr.depth + return self.ptr.depth - property is_alpha: + @property + def is_alpha(self): """Is this component an alpha channel?""" - def __get__(self): - return ((self.index == 1 and self.format.ptr.nb_components == 2) or - (self.index == 3 and self.format.ptr.nb_components == 4)) + return ((self.index == 1 and self.format.ptr.nb_components == 2) or + (self.index == 3 and self.format.ptr.nb_components == 4)) - property is_luma: + @property + def is_luma(self): """Is this compoment a luma channel?""" - def __get__(self): - return self.index == 0 and ( - self.format.ptr.nb_components == 1 or - self.format.ptr.nb_components == 2 or - not self.format.is_rgb - ) - - property is_chroma: + return self.index == 0 and ( + self.format.ptr.nb_components == 1 or + self.format.ptr.nb_components == 2 or + not self.format.is_rgb + ) + + @property + def is_chroma(self): """Is this component a chroma channel?""" - def __get__(self): - return (self.index == 1 or self.index == 2) and (self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h) + return (self.index == 1 or self.index == 2) and (self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h) - property width: + @property + def width(self): """The width of this component's plane. Requires the parent :class:`VideoFormat` to have a width. """ - def __get__(self): - return self.format.chroma_width() if self.is_chroma else self.format.width + return self.format.chroma_width() if self.is_chroma else self.format.width - property height: + @property + def height(self): """The height of this component's plane. Requires the parent :class:`VideoFormat` to have a height. """ - def __get__(self): - return self.format.chroma_height() if self.is_chroma else self.format.height + return self.format.chroma_height() if self.is_chroma else self.format.height names = set() diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 709775e55..53a154a9c 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport uint8_t cimport libav as lib +from libc.stdint cimport uint8_t from av.frame cimport Frame from av.video.format cimport VideoFormat @@ -11,6 +11,7 @@ cdef class VideoFrame(Frame): # This is the buffer that is used to back everything in the AVFrame. # We don't ever actually access it directly. cdef uint8_t *_buffer + cdef object _np_buffer cdef VideoReformatter reformatter diff --git a/av/video/frame.pyi b/av/video/frame.pyi new file mode 100644 index 000000000..da6575328 --- /dev/null +++ b/av/video/frame.pyi @@ -0,0 +1,66 @@ +from typing import Any, Union + +import numpy as np +from PIL import Image + +from av.enum import EnumItem +from av.frame import Frame + +from .format import VideoFormat +from .plane import VideoPlane +from .reformatter import ColorRange, Colorspace + +_SupportedNDarray = Union[ + np.ndarray[Any, np.dtype[np.uint8]], + np.ndarray[Any, np.dtype[np.uint16]], + np.ndarray[Any, np.dtype[np.float32]], +] + +class PictureType(EnumItem): + NONE: int + I: int + P: int + B: int + S: int + SI: int + SP: int + BI: int + +class VideoFrame(Frame): + format: VideoFormat + pts: int + time: float + planes: tuple[VideoPlane, ...] + width: int + height: int + key_frame: bool + interlaced_frame: bool + pict_type: int + colorspace: int + color_range: int + + def __init__( + self, width: int = 0, height: int = 0, format: str = "yuv420p" + ) -> None: ... + def reformat( + self, + width: int | None = None, + height: int | None = None, + format: str | None = None, + src_colorspace: Colorspace | None = None, + dst_colorspace: Colorspace | None = None, + interpolation: int | str | None = None, + src_color_range: int | str | None = None, + dst_color_range: int | str | None = None, + ) -> VideoFrame: ... + def to_rgb(self, **kwargs: Any) -> VideoFrame: ... + def to_image(self, **kwargs: Any) -> Image.Image: ... + def to_ndarray(self, **kwargs: Any) -> _SupportedNDarray: ... + @staticmethod + def from_image(img: Image.Image) -> VideoFrame: ... + @staticmethod + def from_numpy_buffer( + array: _SupportedNDarray, format: str = "rgb24" + ) -> VideoFrame: ... + @staticmethod + def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 971b88ccc..4425e18a9 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -8,6 +8,10 @@ from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane +import warnings + +from av.deprecation import AVDeprecationWarning + cdef object _cinit_bypass_sentinel @@ -21,20 +25,20 @@ cdef VideoFrame alloc_video_frame(): return VideoFrame.__new__(VideoFrame, _cinit_bypass_sentinel) -PictureType = define_enum('PictureType', __name__, ( - ('NONE', lib.AV_PICTURE_TYPE_NONE, "Undefined"), - ('I', lib.AV_PICTURE_TYPE_I, "Intra"), - ('P', lib.AV_PICTURE_TYPE_P, "Predicted"), - ('B', lib.AV_PICTURE_TYPE_B, "Bi-directional predicted"), - ('S', lib.AV_PICTURE_TYPE_S, "S(GMC)-VOP MPEG-4"), - ('SI', lib.AV_PICTURE_TYPE_SI, "Switching intra"), - ('SP', lib.AV_PICTURE_TYPE_SP, "Switching predicted"), - ('BI', lib.AV_PICTURE_TYPE_BI, "BI type"), +PictureType = define_enum("PictureType", __name__, ( + ("NONE", lib.AV_PICTURE_TYPE_NONE, "Undefined"), + ("I", lib.AV_PICTURE_TYPE_I, "Intra"), + ("P", lib.AV_PICTURE_TYPE_P, "Predicted"), + ("B", lib.AV_PICTURE_TYPE_B, "Bi-directional predicted"), + ("S", lib.AV_PICTURE_TYPE_S, "S(GMC)-VOP MPEG-4"), + ("SI", lib.AV_PICTURE_TYPE_SI, "Switching intra"), + ("SP", lib.AV_PICTURE_TYPE_SP, "Switching predicted"), + ("BI", lib.AV_PICTURE_TYPE_BI, "BI type"), )) cdef byteswap_array(array, bint big_endian): - if (sys.byteorder == 'big') != big_endian: + if (sys.byteorder == "big") != big_endian: return array.byteswap() else: return array @@ -57,7 +61,7 @@ cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): o_pos += o_stride -cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype='uint8'): +cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="uint8"): """ Return the useful part of the VideoPlane as a single dimensional array. @@ -73,9 +77,7 @@ cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype='u cdef class VideoFrame(Frame): - - def __cinit__(self, width=0, height=0, format='yuv420p'): - + def __cinit__(self, width=0, height=0, format="yuv420p"): if width is _cinit_bypass_sentinel: return @@ -84,7 +86,6 @@ cdef class VideoFrame(Frame): self._init(c_format, width, height) cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height): - cdef int res = 0 with nogil: @@ -118,16 +119,13 @@ cdef class VideoFrame(Frame): # The `self._buffer` member is only set if *we* allocated the buffer in `_init`, # as opposed to a buffer allocated by a decoder. lib.av_freep(&self._buffer) + # Let go of the reference from the numpy buffers if we made one + self._np_buffer = None def __repr__(self): - return '' % ( - self.__class__.__name__, - self.index, - self.pts, - self.format.name, - self.width, - self.height, - id(self), + return ( + f"" ) @property @@ -144,38 +142,45 @@ cdef class VideoFrame(Frame): count = self.format.ptr.comp[i].plane + 1 if max_plane_count < count: max_plane_count = count - if self.format.name == 'pal8': + if self.format.name == "pal8": max_plane_count = 2 cdef int plane_count = 0 while plane_count < max_plane_count and self.ptr.extended_data[plane_count]: plane_count += 1 - return tuple([VideoPlane(self, i) for i in range(plane_count)]) - property width: + @property + def width(self): """Width of the image, in pixels.""" - def __get__(self): return self.ptr.width + return self.ptr.width + - property height: + @property + def height(self): """Height of the image, in pixels.""" - def __get__(self): return self.ptr.height + return self.ptr.height - property key_frame: + + @property + def key_frame(self): """Is this frame a key frame? Wraps :ffmpeg:`AVFrame.key_frame`. """ - def __get__(self): return self.ptr.key_frame + return self.ptr.key_frame + - property interlaced_frame: + @property + def interlaced_frame(self): """Is this frame an interlaced or progressive? Wraps :ffmpeg:`AVFrame.interlaced_frame`. """ - def __get__(self): return self.ptr.interlaced_frame + return self.ptr.interlaced_frame + @property def pict_type(self): @@ -190,6 +195,32 @@ cdef class VideoFrame(Frame): def pict_type(self, value): self.ptr.pict_type = PictureType[value].value + @property + def colorspace(self): + """Colorspace of frame. + + Wraps :ffmpeg:`AVFrame.colorspace`. + + """ + return self.ptr.colorspace + + @colorspace.setter + def colorspace(self, value): + self.ptr.colorspace = value + + @property + def color_range(self): + """Color range of frame. + + Wraps :ffmpeg:`AVFrame.color_range`. + + """ + return self.ptr.color_range + + @color_range.setter + def color_range(self, value): + self.ptr.color_range = value + def reformat(self, *args, **kwargs): """reformat(width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, interpolation=None) @@ -261,7 +292,7 @@ cdef class VideoFrame(Frame): import numpy as np - if frame.format.name in ('yuv420p', 'yuvj420p'): + if frame.format.name in ("yuv420p", "yuvj420p"): assert frame.width % 2 == 0 assert frame.height % 2 == 0 return np.hstack(( @@ -269,82 +300,164 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1]), useful_array(frame.planes[2]) )).reshape(-1, frame.width) - elif frame.format.name in ('yuv444p', 'yuvj444p'): + elif frame.format.name in ("yuv444p", "yuvj444p"): return np.hstack(( useful_array(frame.planes[0]), useful_array(frame.planes[1]), useful_array(frame.planes[2]) )).reshape(-1, frame.height, frame.width) - elif frame.format.name == 'yuyv422': + elif frame.format.name == "yuyv422": assert frame.width % 2 == 0 assert frame.height % 2 == 0 return useful_array(frame.planes[0], 2).reshape(frame.height, frame.width, -1) - elif frame.format.name == 'gbrp': + elif frame.format.name == "gbrp": array = np.empty((frame.height, frame.width, 3), dtype="uint8") array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(-1, frame.width) array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(-1, frame.width) array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(-1, frame.width) return array - elif frame.format.name in ('gbrp10be', 'gbrp12be', 'gbrp14be', 'gbrp16be', 'gbrp10le', 'gbrp12le', 'gbrp14le', 'gbrp16le'): + elif frame.format.name in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): array = np.empty((frame.height, frame.width, 3), dtype="uint16") array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(-1, frame.width) array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(-1, frame.width) array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(-1, frame.width) - return byteswap_array(array, frame.format.name.endswith('be')) - elif frame.format.name in ('gbrpf32be', 'gbrpf32le'): + return byteswap_array(array, frame.format.name.endswith("be")) + elif frame.format.name in ("gbrpf32be", "gbrpf32le"): array = np.empty((frame.height, frame.width, 3), dtype="float32") array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(-1, frame.width) array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(-1, frame.width) array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(-1, frame.width) - return byteswap_array(array, frame.format.name.endswith('be')) - elif frame.format.name in ('rgb24', 'bgr24'): + return byteswap_array(array, frame.format.name.endswith("be")) + elif frame.format.name in ("rgb24", "bgr24"): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) - elif frame.format.name in ('argb', 'rgba', 'abgr', 'bgra'): + elif frame.format.name in ("argb", "rgba", "abgr", "bgra"): return useful_array(frame.planes[0], 4).reshape(frame.height, frame.width, -1) - elif frame.format.name in ('gray', 'gray8', 'rgb8', 'bgr8'): + elif frame.format.name in ("gray", "gray8", "rgb8", "bgr8"): return useful_array(frame.planes[0]).reshape(frame.height, frame.width) - elif frame.format.name in ('gray16be', 'gray16le'): + elif frame.format.name in ("gray16be", "gray16le"): return byteswap_array( - useful_array(frame.planes[0], 2, 'uint16').reshape(frame.height, frame.width), - frame.format.name == 'gray16be', + useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width), + frame.format.name == "gray16be", ) - elif frame.format.name in ('rgb48be', 'rgb48le'): + elif frame.format.name in ("rgb48be", "rgb48le"): return byteswap_array( - useful_array(frame.planes[0], 6, 'uint16').reshape(frame.height, frame.width, -1), - frame.format.name == 'rgb48be', + useful_array(frame.planes[0], 6, "uint16").reshape(frame.height, frame.width, -1), + frame.format.name == "rgb48be", ) - elif frame.format.name in ('rgba64be', 'rgba64le'): + elif frame.format.name in ("rgba64be", "rgba64le"): return byteswap_array( - useful_array(frame.planes[0], 8, 'uint16').reshape(frame.height, frame.width, -1), - frame.format.name == 'rgba64be', + useful_array(frame.planes[0], 8, "uint16").reshape(frame.height, frame.width, -1), + frame.format.name == "rgba64be", ) - elif frame.format.name == 'pal8': + elif frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) - palette = np.frombuffer(frame.planes[1], 'i4').astype('>i4').reshape(-1, 1).view(np.uint8) + palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) return image, palette - elif frame.format.name == 'nv12': + elif frame.format.name == "nv12": return np.hstack(( useful_array(frame.planes[0]), useful_array(frame.planes[1], 2) )).reshape(-1, frame.width) else: - raise ValueError('Conversion to numpy array with format `%s` is not yet supported' % frame.format.name) + raise ValueError( + f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" + ) @staticmethod def from_image(img): """ Construct a frame from a ``PIL.Image``. """ - if img.mode != 'RGB': - img = img.convert('RGB') + if img.mode != "RGB": + img = img.convert("RGB") - cdef VideoFrame frame = VideoFrame(img.size[0], img.size[1], 'rgb24') + cdef VideoFrame frame = VideoFrame(img.size[0], img.size[1], "rgb24") copy_array_to_plane(img, frame.planes[0], 3) return frame @staticmethod - def from_ndarray(array, format='rgb24'): + def from_numpy_buffer(array, format="rgb24"): + if format in ("rgb24", "bgr24"): + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 3) + height, width = array.shape[:2] + elif format in ("gray", "gray8", "rgb8", "bgr8"): + check_ndarray(array, "uint8", 2) + height, width = array.shape[:2] + elif format in ("yuv420p", "yuvj420p", "nv12"): + check_ndarray(array, "uint8", 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + height, width = array.shape[:2] + height = height // 6 * 4 + else: + raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") + + if not array.flags["C_CONTIGUOUS"]: + raise ValueError("provided array must be C_CONTIGUOUS") + + frame = alloc_video_frame() + frame._image_fill_pointers_numpy(array, width, height, format) + return frame + + def _image_fill_pointers_numpy(self, buffer, width, height, format): + cdef lib.AVPixelFormat c_format + cdef uint8_t * c_ptr + cdef size_t c_data + + # If you want to use the numpy notation + # then you need to include the following two lines at the top of the file + # cimport numpy as cnp + # cnp.import_array() + # And add the numpy include directories to the setup.py files + # hint np.get_include() + # cdef cnp.ndarray[ + # dtype=cnp.uint8_t, ndim=1, + # negative_indices=False, mode='c'] c_buffer + # c_buffer = buffer.reshape(-1) + # c_ptr = &c_buffer[0] + # c_ptr = ((buffer.ctypes.data)) + + # Using buffer.ctypes.data helps avoid any kind of + # usage of the c-api from numpy, which avoid the need to add numpy + # as a compile time dependency + # Without this double cast, you get an error that looks like + # c_ptr = (buffer.ctypes.data) + # TypeError: expected bytes, int found + c_data = buffer.ctypes.data + c_ptr = (c_data) + c_format = get_pix_fmt(format) + lib.av_freep(&self._buffer) + + # Hold on to a reference for the numpy buffer + # so that it doesn't get accidentally garbage collected + self._np_buffer = buffer + self.ptr.format = c_format + self.ptr.width = width + self.ptr.height = height + res = lib.av_image_fill_linesizes( + self.ptr.linesize, + self.ptr.format, + width, + ) + if res: + err_check(res) + + res = lib.av_image_fill_pointers( + self.ptr.data, + self.ptr.format, + self.ptr.height, + c_ptr, + self.ptr.linesize, + ) + + if res: + err_check(res) + self._init_user_attributes() + + @staticmethod + def from_ndarray(array, format="rgb24"): """ Construct a frame from a numpy array. @@ -355,18 +468,18 @@ cdef class VideoFrame(Frame): `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). """ - if format == 'pal8': + if format == "pal8": array, palette = array - check_ndarray(array, 'uint8', 2) - check_ndarray(palette, 'uint8', 2) + check_ndarray(array, "uint8", 2) + check_ndarray(palette, "uint8", 2) check_ndarray_shape(palette, palette.shape == (256, 4)) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(array, frame.planes[0], 1) - frame.planes[1].update(palette.view('>i4').astype('i4').tobytes()) + frame.planes[1].update(palette.view(">i4").astype("i4").tobytes()) return frame - elif format in ('yuv420p', 'yuvj420p'): - check_ndarray(array, 'uint8', 2) + elif format in ("yuv420p", "yuvj420p"): + check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) @@ -378,8 +491,8 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame - elif format in ('yuv444p', 'yuvj444p'): - check_ndarray(array, 'uint8', 3) + elif format in ("yuv444p", "yuvj444p"): + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] == 3) frame = VideoFrame(array.shape[2], array.shape[1], format) @@ -388,13 +501,13 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array[1], frame.planes[1], 1) copy_array_to_plane(array[2], frame.planes[2], 1) return frame - elif format == 'yuyv422': - check_ndarray(array, 'uint8', 3) + elif format == "yuyv422": + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] % 2 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) check_ndarray_shape(array, array.shape[2] == 2) - elif format == 'gbrp': - check_ndarray(array, 'uint8', 3) + elif format == "gbrp": + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) @@ -402,51 +515,51 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array[:, :, 2], frame.planes[1], 1) copy_array_to_plane(array[:, :, 0], frame.planes[2], 1) return frame - elif format in ('gbrp10be', 'gbrp12be', 'gbrp14be', 'gbrp16be', 'gbrp10le', 'gbrp12le', 'gbrp14le', 'gbrp16le'): - check_ndarray(array, 'uint16', 3) + elif format in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): + check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith('be')), frame.planes[0], 2) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith('be')), frame.planes[1], 2) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith('be')), frame.planes[2], 2) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 2) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 2) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 2) return frame - elif format in ('gbrpf32be', 'gbrpf32le'): - check_ndarray(array, 'float32', 3) + elif format in ("gbrpf32be", "gbrpf32le"): + check_ndarray(array, "float32", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith('be')), frame.planes[0], 4) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith('be')), frame.planes[1], 4) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith('be')), frame.planes[2], 4) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) return frame - elif format in ('rgb24', 'bgr24'): - check_ndarray(array, 'uint8', 3) + elif format in ("rgb24", "bgr24"): + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) - elif format in ('argb', 'rgba', 'abgr', 'bgra'): - check_ndarray(array, 'uint8', 3) + elif format in ("argb", "rgba", "abgr", "bgra"): + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in ('gray', 'gray8', 'rgb8', 'bgr8'): - check_ndarray(array, 'uint8', 2) - elif format in ('gray16be', 'gray16le'): - check_ndarray(array, 'uint16', 2) + elif format in ("gray", "gray8", "rgb8", "bgr8"): + check_ndarray(array, "uint8", 2) + elif format in ("gray16be", "gray16le"): + check_ndarray(array, "uint16", 2) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == 'gray16be'), frame.planes[0], 2) + copy_array_to_plane(byteswap_array(array, format == "gray16be"), frame.planes[0], 2) return frame - elif format in ('rgb48be', 'rgb48le'): - check_ndarray(array, 'uint16', 3) + elif format in ("rgb48be", "rgb48le"): + check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == 'rgb48be'), frame.planes[0], 6) + copy_array_to_plane(byteswap_array(array, format == "rgb48be"), frame.planes[0], 6) return frame - elif format in ('rgba64be', 'rgba64le'): - check_ndarray(array, 'uint16', 3) + elif format in ("rgba64be", "rgba64le"): + check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == 'rgba64be'), frame.planes[0], 8) + copy_array_to_plane(byteswap_array(array, format == "rgba64be"), frame.planes[0], 8) return frame - elif format == 'nv12': - check_ndarray(array, 'uint8', 2) + elif format == "nv12": + check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) @@ -457,9 +570,16 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[uv_start:], frame.planes[1], 2) return frame else: - raise ValueError('Conversion from numpy array with format `%s` is not yet supported' % format) + raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) return frame + + def __getattribute__(self, attribute): + # This method should be deleted when `frame.index` is removed + if attribute == "index": + warnings.warn("Using `frame.index` is deprecated.", AVDeprecationWarning) + + return Frame.__getattribute__(self, attribute) diff --git a/av/video/plane.pyi b/av/video/plane.pyi new file mode 100644 index 000000000..9bc1f0d77 --- /dev/null +++ b/av/video/plane.pyi @@ -0,0 +1,9 @@ +from .frame import VideoFrame + +class VideoPlane: + line_size: int + width: int + height: int + buffer_size: int + + def __init__(self, frame: VideoFrame, index: int) -> None: ... diff --git a/av/video/plane.pyx b/av/video/plane.pyx index 6f1286ca3..908b48716 100644 --- a/av/video/plane.pyx +++ b/av/video/plane.pyx @@ -2,11 +2,9 @@ from av.video.frame cimport VideoFrame cdef class VideoPlane(Plane): - def __cinit__(self, VideoFrame frame, int index): - # The palette plane has no associated component or linesize; set fields manually - if frame.format.name == 'pal8' and index == 1: + if frame.format.name == "pal8" and index == 1: self.width = 256 self.height = 1 self.buffer_size = 256 * 4 @@ -19,7 +17,7 @@ cdef class VideoPlane(Plane): self.height = component.height break else: - raise RuntimeError('could not find plane %d of %r' % (index, frame.format)) + raise RuntimeError(f"could not find plane {index} of {frame.format!r}") # Sometimes, linesize is negative (and that is meaningful). We are only # insisting that the buffer size be based on the extent of linesize, and @@ -29,11 +27,11 @@ cdef class VideoPlane(Plane): cdef size_t _buffer_size(self): return self.buffer_size - property line_size: + @property + def line_size(self): """ Bytes per horizontal line in this plane. :type: int """ - def __get__(self): - return self.frame.ptr.linesize[self.index] + return self.frame.ptr.linesize[self.index] diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index ee7467898..7682fab6d 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -9,4 +9,5 @@ cdef class VideoReformatter: cdef _reformat(self, VideoFrame frame, int width, int height, lib.AVPixelFormat format, int src_colorspace, - int dst_colorspace, int interpolation) + int dst_colorspace, int interpolation, + int src_color_range, int dst_color_range) diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi new file mode 100644 index 000000000..a68dc9718 --- /dev/null +++ b/av/video/reformatter.pyi @@ -0,0 +1,51 @@ +from av.enum import EnumItem + +from .frame import VideoFrame + +class Interpolation(EnumItem): + FAST_BILINEAER: int + BILINEAR: int + BICUBIC: int + X: int + POINT: int + AREA: int + BICUBLIN: int + GAUSS: int + SINC: int + LANCZOS: int + SPLINE: int + +class Colorspace(EnumItem): + ITU709: int + FCC: int + ITU601: int + ITU624: int + SMPTE170M: int + SMPTE240M: int + DEFAULT: int + itu709: int + fcc: int + itu601: int + itu624: int + smpte240: int + default: int + +class ColorRange(EnumItem): + UNSPECIFIED: int + MPEG: int + JPEG: int + NB: int + +class VideoReformatter: + def reformat( + self, + frame: VideoFrame, + width: int | None = None, + height: int | None = None, + format: str | None = None, + src_colorspace: Colorspace | None = None, + dst_colorspace: Colorspace | None = None, + interpolation: int | str | None = None, + src_color_range: int | str | None = None, + dst_color_range: int | str | None = None, + ) -> VideoFrame: ... diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 1d3f08065..f41094eda 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -1,49 +1,52 @@ -from libc.stdint cimport uint8_t cimport libav as lib +from libc.stdint cimport uint8_t from av.enum cimport define_enum from av.error cimport err_check from av.video.format cimport VideoFormat from av.video.frame cimport alloc_video_frame - -Interpolation = define_enum('Interpolation', __name__, ( - ('FAST_BILINEAR', lib.SWS_FAST_BILINEAR, "Fast bilinear"), - ('BILINEAR', lib.SWS_BILINEAR, "Bilinear"), - ('BICUBIC', lib.SWS_BICUBIC, "Bicubic"), - ('X', lib.SWS_X, "Experimental"), - ('POINT', lib.SWS_POINT, "Nearest neighbor / point"), - ('AREA', lib.SWS_AREA, "Area averaging"), - ('BICUBLIN', lib.SWS_BICUBLIN, "Luma bicubic / chroma bilinear"), - ('GAUSS', lib.SWS_GAUSS, "Gaussian"), - ('SINC', lib.SWS_SINC, "Sinc"), - ('LANCZOS', lib.SWS_LANCZOS, "Lanczos"), - ('SPLINE', lib.SWS_SPLINE, "Bicubic spline"), +Interpolation = define_enum("Interpolation", __name__, ( + ("FAST_BILINEAR", lib.SWS_FAST_BILINEAR, "Fast bilinear"), + ("BILINEAR", lib.SWS_BILINEAR, "Bilinear"), + ("BICUBIC", lib.SWS_BICUBIC, "Bicubic"), + ("X", lib.SWS_X, "Experimental"), + ("POINT", lib.SWS_POINT, "Nearest neighbor / point"), + ("AREA", lib.SWS_AREA, "Area averaging"), + ("BICUBLIN", lib.SWS_BICUBLIN, "Luma bicubic / chroma bilinear"), + ("GAUSS", lib.SWS_GAUSS, "Gaussian"), + ("SINC", lib.SWS_SINC, "Sinc"), + ("LANCZOS", lib.SWS_LANCZOS, "Lanczos"), + ("SPLINE", lib.SWS_SPLINE, "Bicubic spline"), )) -Colorspace = define_enum('Colorspace', __name__, ( - - ('ITU709', lib.SWS_CS_ITU709), - ('FCC', lib.SWS_CS_FCC), - ('ITU601', lib.SWS_CS_ITU601), - ('ITU624', lib.SWS_CS_ITU624), - ('SMPTE170M', lib.SWS_CS_SMPTE170M), - ('SMPTE240M', lib.SWS_CS_SMPTE240M), - ('DEFAULT', lib.SWS_CS_DEFAULT), +Colorspace = define_enum("Colorspace", __name__, ( + ("ITU709", lib.SWS_CS_ITU709), + ("FCC", lib.SWS_CS_FCC), + ("ITU601", lib.SWS_CS_ITU601), + ("ITU624", lib.SWS_CS_ITU624), + ("SMPTE170M", lib.SWS_CS_SMPTE170M), + ("SMPTE240M", lib.SWS_CS_SMPTE240M), + ("DEFAULT", lib.SWS_CS_DEFAULT), # Lowercase for b/c. - ('itu709', lib.SWS_CS_ITU709), - ('fcc', lib.SWS_CS_FCC), - ('itu601', lib.SWS_CS_ITU601), - ('itu624', lib.SWS_CS_SMPTE170M), - ('smpte240', lib.SWS_CS_SMPTE240M), - ('default', lib.SWS_CS_DEFAULT), + ("itu709", lib.SWS_CS_ITU709), + ("fcc", lib.SWS_CS_FCC), + ("itu601", lib.SWS_CS_ITU601), + ("itu624", lib.SWS_CS_SMPTE170M), + ("smpte240", lib.SWS_CS_SMPTE240M), + ("default", lib.SWS_CS_DEFAULT), )) +ColorRange = define_enum("ColorRange", __name__, ( + ("UNSPECIFIED", lib.AVCOL_RANGE_UNSPECIFIED, "Unspecified"), + ("MPEG", lib.AVCOL_RANGE_MPEG, "MPEG (limited) YUV range, 219*2^(n-8)"), + ("JPEG", lib.AVCOL_RANGE_JPEG, "JPEG (full) YUV range, 2^n-1"), + ("NB", lib.AVCOL_RANGE_NB, "Not part of ABI"), +)) cdef class VideoReformatter: - """An object for reformatting size and pixel format of :class:`.VideoFrame`. It is most efficient to have a reformatter object for each set of parameters @@ -57,7 +60,8 @@ cdef class VideoReformatter: def reformat(self, VideoFrame frame not None, width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, - interpolation=None): + interpolation=None, src_color_range=None, + dst_color_range=None): """Create a new :class:`VideoFrame` with the given width/height/format/colorspace. Returns the same frame untouched if nothing needs to be done to it. @@ -66,19 +70,25 @@ cdef class VideoReformatter: :param int height: New height, or ``None`` for the same height. :param format: New format, or ``None`` for the same format. :type format: :class:`.VideoFormat` or ``str`` - :param src_colorspace: Current colorspace, or ``None`` for ``DEFAULT``. + :param src_colorspace: Current colorspace, or ``None`` for the frame colorspace. :type src_colorspace: :class:`Colorspace` or ``str`` - :param dst_colorspace: Desired colorspace, or ``None`` for ``DEFAULT``. + :param dst_colorspace: Desired colorspace, or ``None`` for the frame colorspace. :type dst_colorspace: :class:`Colorspace` or ``str`` :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. :type interpolation: :class:`Interpolation` or ``str`` + :param src_color_range: Current color range, or ``None`` for the frame color range. + :type src_color_range: :class:`color range` or ``str`` + :param dst_color_range: Desired color range, or ``None`` for the frame color range. + :type dst_color_range: :class:`color range` or ``str`` """ cdef VideoFormat video_format = VideoFormat(format if format is not None else frame.format) - cdef int c_src_colorspace = (Colorspace[src_colorspace] if src_colorspace is not None else Colorspace.DEFAULT).value - cdef int c_dst_colorspace = (Colorspace[dst_colorspace] if dst_colorspace is not None else Colorspace.DEFAULT).value + cdef int c_src_colorspace = (Colorspace[src_colorspace].value if src_colorspace is not None else frame.colorspace) + cdef int c_dst_colorspace = (Colorspace[dst_colorspace].value if dst_colorspace is not None else frame.colorspace) cdef int c_interpolation = (Interpolation[interpolation] if interpolation is not None else Interpolation.BILINEAR).value + cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else frame.color_range) + cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else frame.color_range) return self._reformat( frame, @@ -88,11 +98,14 @@ cdef class VideoReformatter: c_src_colorspace, c_dst_colorspace, c_interpolation, + c_src_color_range, + c_dst_color_range, ) cdef _reformat(self, VideoFrame frame, int width, int height, lib.AVPixelFormat dst_format, int src_colorspace, - int dst_colorspace, int interpolation): + int dst_colorspace, int interpolation, + int src_color_range, int dst_color_range): if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") @@ -104,7 +117,8 @@ cdef class VideoReformatter: dst_format == src_format and width == frame.ptr.width and height == frame.ptr.height and - dst_colorspace == src_colorspace + dst_colorspace == src_colorspace and + src_color_range == dst_color_range ): return frame @@ -126,24 +140,24 @@ cdef class VideoReformatter: NULL ) - # We want to change the colorspace transforms. We do that by grabbing - # all of the current settings, changing a couple, and setting them all. - # We need a lot of state here. + # We want to change the colorspace/color_range transforms. + # We do that by grabbing all of the current settings, changing a + # couple, and setting them all. We need a lot of state here. cdef const int *inv_tbl cdef const int *tbl - cdef int src_range, dst_range, brightness, contrast, saturation + cdef int src_colorspace_range, dst_colorspace_range + cdef int brightness, contrast, saturation cdef int ret - if src_colorspace != dst_colorspace: + if src_colorspace != dst_colorspace or src_color_range != dst_color_range: with nogil: - # Casts for const-ness, because Cython isn't expressive enough. ret = lib.sws_getColorspaceDetails( self.ptr, &inv_tbl, - &src_range, + &src_colorspace_range, &tbl, - &dst_range, + &dst_colorspace_range, &brightness, &contrast, &saturation @@ -152,7 +166,6 @@ cdef class VideoReformatter: err_check(ret) with nogil: - # Grab the coefficients for the requested transforms. # The inv_table brings us to linear, and `tbl` to the new space. if src_colorspace != lib.SWS_CS_DEFAULT: @@ -164,9 +177,9 @@ cdef class VideoReformatter: ret = lib.sws_setColorspaceDetails( self.ptr, inv_tbl, - src_range, + src_color_range, tbl, - dst_range, + dst_color_range, brightness, contrast, saturation diff --git a/av/video/stream.pyi b/av/video/stream.pyi new file mode 100644 index 000000000..b0266328a --- /dev/null +++ b/av/video/stream.pyi @@ -0,0 +1,28 @@ +from fractions import Fraction +from typing import Any, Literal + +from av.packet import Packet +from av.stream import Stream + +from .codeccontext import VideoCodecContext +from .format import VideoFormat +from .frame import VideoFrame + +class VideoStream(Stream): + width: int + height: int + format: VideoFormat + pix_fmt: str | None + sample_aspect_ratio: Fraction | None + codec_context: VideoCodecContext + type: Literal["video"] + + # from codec context + bit_rate: int | None + max_bit_rate: int | None + bit_rate_tolerance: int + thread_count: int + thread_type: Any + + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... # type: ignore[override] + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 8694b63ba..08949be2e 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,12 +1,7 @@ cdef class VideoStream(Stream): - def __repr__(self): - return '' % ( - self.__class__.__name__, - self.index, - self.name, - self.format.name if self.format else None, - self.codec_context.width, - self.codec_context.height, - id(self), + return ( + f"" ) diff --git a/docs/Makefile b/docs/Makefile index 65a1e540e..9ebbf3c5d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,6 +2,7 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build +FFMPEGDIR = _ffmpeg ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . @@ -12,7 +13,8 @@ default: html TAGFILE := _build/doxygen/tagfile.xml $(TAGFILE) : - ./generate-tagfile -o $(TAGFILE) + git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR) + ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) TEMPLATES := $(wildcard api/*.py development/*.py) @@ -24,7 +26,7 @@ _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -o clean: - - rm -rf $(BUILDDIR)/* + rm -rf $(BUILDDIR) $(FFMPEGDIR) html: $(RENDERED) $(TAGFILE) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @@ -36,5 +38,5 @@ open: open _build/html/index.html upload: - rsync -avxP --delete _build/html/ pyav.org:/srv/pyav.org/www/httpdocs/docs/develop/ + rsync -avxP --delete _build/html/ root@basswood-io.com:/var/www/pyav/docs/develop diff --git a/docs/api/time.rst b/docs/api/time.rst index 35e4cfc85..fd65de1a2 100644 --- a/docs/api/time.rst +++ b/docs/api/time.rst @@ -38,7 +38,7 @@ Attributes that represent time on those objects will be in that object's ``time_ >>> float(video.duration * video.time_base) 6.72 -:class:`.Packet` has a :attr:`.Packet.pts` ("presentation" time stamp), and :class:`.Frame` has a :attr:`.Frame.pts` and :attr:`.Frame.dts` ("presentation" and "decode" time stamps). Both have a ``time_base`` attribute, but it defaults to the time base of the object that handles them. For packets that is streams. For frames it is streams when decoding, and codec contexts when encoding (which is strange, but it is what it is). +:class:`.Packet` has a :attr:`.Packet.pts` and :attr:`.Packet.dts` ("presentation" and "decode" time stamps), and :class:`.Frame` has a :attr:`.Frame.pts` ("presentation" time stamp). Both have a ``time_base`` attribute, but it defaults to the time base of the object that handles them. For packets that is streams. For frames it is streams when decoding, and codec contexts when encoding (which is strange, but it is what it is). In many cases a stream has a time base of ``1 / frame_rate``, and then its frames have incrementing integers for times (0, 1, 2, etc.). Those frames take place at ``pts * time_base`` or ``0 / frame_rate``, ``1 / frame_rate``, ``2 / frame_rate``, etc.. diff --git a/docs/conf.py b/docs/conf.py index cc97c4397..a893a631c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,206 +1,90 @@ -# -*- coding: utf-8 -*- -# -# PyAV documentation build configuration file, created by -# sphinx-quickstart on Fri Dec 7 22:13:16 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -from docutils import nodes import logging -import math import os import re import sys -import sys import xml.etree.ElementTree as etree -import sphinx +from docutils import nodes from sphinx import addnodes from sphinx.util.docutils import SphinxDirective +import sphinx logging.basicConfig() - -if sphinx.version_info < (1, 8): - print("Sphinx {} is too old; we require >= 1.8.".format(sphinx.__version__), file=sys.stderr) - exit(1) - - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.extlinks', - 'sphinx.ext.doctest', - - # We used to use doxylink, but we found its caching behaviour annoying, and - # so made a minimally viable version of our own. + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.extlinks", + "sphinx.ext.doctest", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'PyAV' -copyright = u'2017, Mike Boers' +project = "PyAV" +copyright = "2024, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # about = {} -with open('../av/about.py') as fp: +with open("../av/about.py") as fp: exec(fp.read(), about) # The full version, including alpha/beta/rc tags. -release = about['__version__'] +release = about["__version__"] # The short X.Y version. -version = release.split('-')[0] - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None +version = release.split("-")[0] -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False +exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - +pygments_style = "sphinx" # -- Options for HTML output --------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'pyav' -html_theme_path = [os.path.abspath(os.path.join(__file__, '..', '_themes'))] -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +html_theme = "pyav" +html_theme_path = [os.path.abspath(os.path.join(__file__, "..", "_themes"))] # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_static/logo-250.png' +html_logo = "_static/logo-250.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = '_static/favicon.png' +html_favicon = "_static/favicon.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True +html_static_path = ["_static"] -# If true, the index is split into individual pages for each letter. -#html_split_index = False -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -doctest_global_setup = ''' +doctest_global_setup = """ import errno import os @@ -226,33 +110,33 @@ def sandboxed(*args, **kwargs): video_path = curated('pexels/time-lapse-video-of-night-sky-857195.mp4') -''' +""" -doctest_global_cleanup = ''' +doctest_global_cleanup = """ os.chdir(_cwd) -''' +""" -doctest_test_doctest_blocks = '' +doctest_test_doctest_blocks = "" extlinks = { - 'ffstruct': ('http://ffmpeg.org/doxygen/trunk/struct%s.html', 'struct '), - 'issue': ('https://github.com/PyAV-Org/PyAV/issues/%s', '#'), - 'pr': ('https://github.com/PyAV-Org/PyAV/pull/%s', '#'), - 'gh-user': ('https://github.com/%s', '@'), + "ffstruct": ("http://ffmpeg.org/doxygen/trunk/struct%s.html", "struct "), + "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#"), + "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#"), + "gh-user": ("https://github.com/%s", "@"), } intersphinx_mapping = { - 'https://docs.python.org/3': None, + "https://docs.python.org/3": None, } -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" autodoc_default_options = { - 'undoc-members': True, - 'show-inheritance': True, + "undoc-members": True, + "show-inheritance": True, } @@ -260,57 +144,54 @@ def sandboxed(*args, **kwargs): class PyInclude(SphinxDirective): - has_content = True def run(self): - - - source = '\n'.join(self.content) + source = "\n".join(self.content) output = [] - def write(*content, sep=' ', end='\n'): + + def write(*content, sep=" ", end="\n"): output.append(sep.join(map(str, content)) + end) namespace = dict(write=write) - exec(compile(source, '', 'exec'), namespace, namespace) + exec(compile(source, "", "exec"), namespace, namespace) - output = ''.join(output).splitlines() - self.state_machine.insert_input(output, 'blah') + output = "".join(output).splitlines() + self.state_machine.insert_input(output, "blah") - return [] #[nodes.literal('hello', repr(content))] + return [] # [nodes.literal('hello', repr(content))] def load_entrypoint(name): - - parts = name.split(':') + parts = name.split(":") if len(parts) == 1: - parts = name.rsplit('.', 1) + parts = name.rsplit(".", 1) mod_name, attrs = parts - attrs = attrs.split('.') + attrs = attrs.split(".") try: - obj = __import__(mod_name, fromlist=['.']) + obj = __import__(mod_name, fromlist=["."]) except ImportError as e: - print('Error while importing.', (name, mod_name, attrs, e)) + print("Error while importing.", (name, mod_name, attrs, e)) raise + for attr in attrs: obj = getattr(obj, attr) + return obj -class EnumTable(SphinxDirective): +class EnumTable(SphinxDirective): required_arguments = 1 option_spec = { - 'class': lambda x: x, + "class": lambda x: x, } def run(self): - - cls_ep = self.options.get('class') + cls_ep = self.options.get("class") cls = load_entrypoint(cls_ep) if cls_ep else None enum = load_entrypoint(self.arguments[0]) - properties = {} if cls is not None: @@ -346,20 +227,19 @@ def makerow(*texts): for text in texts: if text is None: continue - row += nodes.entry('', nodes.paragraph('', str(text))) + row += nodes.entry("", nodes.paragraph("", str(text))) return row thead += makerow( - '{} Attribute'.format(cls.__name__) if cls else None, - '{} Name'.format(enum.__name__), - 'Flag Value', - 'Meaning in FFmpeg', + f"{cls.__name__} Attribute" if cls else None, + f"{enum.__name__} Name", + "Flag Value", + "Meaning in FFmpeg", ) seen = set() for name, item in enum._by_name.items(): - if name.lower() in seen: continue seen.add(name.lower()) @@ -371,32 +251,24 @@ def makerow(*texts): continue attr = None - value = '0x{:X}'.format(item.value) - - doc = item.__doc__ or '-' - - tbody += makerow( - attr, - name, - value, - doc, - ) + value = f"0x{item.value:X}" + doc = item.__doc__ or "-" + tbody += makerow(attr, name, value, doc) return [table] - - doxylink = {} -ffmpeg_tagfile = os.path.abspath(os.path.join(__file__, '..', '_build', 'doxygen', 'tagfile.xml')) +ffmpeg_tagfile = os.path.abspath( + os.path.join(__file__, "..", "_build", "doxygen", "tagfile.xml") +) if not os.path.exists(ffmpeg_tagfile): print("ERROR: Missing FFmpeg tagfile.") exit(1) -doxylink['ffmpeg'] = (ffmpeg_tagfile, 'https://ffmpeg.org/doxygen/trunk/') +doxylink["ffmpeg"] = (ffmpeg_tagfile, "https://ffmpeg.org/doxygen/trunk/") def doxylink_create_handler(app, file_name, url_base): - print("Finding all names in Doxygen tagfile", file_name) doc = etree.parse(file_name) @@ -405,30 +277,29 @@ def doxylink_create_handler(app, file_name, url_base): parent_map = {} # ElementTree doesn't five us access to parents. urls = {} - for node in root.findall('.//name/..'): - + for node in root.findall(".//name/.."): for child in node: parent_map[child] = node - kind = node.attrib['kind'] - if kind not in ('function', 'struct', 'variable'): + kind = node.attrib["kind"] + if kind not in ("function", "struct", "variable"): continue - name = node.find('name').text + name = node.find("name").text - if kind not in ('function', ): + if kind not in ("function",): parent = parent_map.get(node) - parent_name = parent.find('name') if parent else None + parent_name = parent.find("name") if parent else None if parent_name is not None: - name = '{}.{}'.format(parent_name.text, name) + name = f"{parent_name.text}.{name}" - filenode = node.find('filename') + filenode = node.find("filename") if filenode is not None: url = filenode.text else: - url = '{}#{}'.format( - node.find('anchorfile').text, - node.find('anchor').text, + url = "{}#{}".format( + node.find("anchorfile").text, + node.find("anchor").text, ) urls.setdefault(kind, {})[name] = url @@ -436,9 +307,9 @@ def doxylink_create_handler(app, file_name, url_base): def get_url(name): # These are all the kinds that seem to exist. for kind in ( - 'function', - 'struct', - 'variable', # These are struct members. + "function", + "struct", + "variable", # These are struct members. # 'class', # 'define', # 'enumeration', @@ -454,45 +325,41 @@ def get_url(name): except KeyError: pass - def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content=[]): - - m = re.match(r'^(.+?)(?:<(.+?)>)?$', text) + m = re.match(r"^(.+?)(?:<(.+?)>)?$", text) title, name = m.groups() name = name or title url = get_url(name) if not url: - print("ERROR: Could not find", name) - exit(1) + if name == "AVFrame.color_primaries": + url = "structAVFrame.html#a59a3f830494f2ed1133103a1bc9481e7" + elif name == "AVFrame.color_trc": + url = "structAVFrame.html#ab09abb126e3922bc1d010cf044087939" + else: + print("ERROR: Could not find", name) + exit(1) node = addnodes.literal_strong(title, title) if url: url = url_base + url - node = nodes.reference( - '', '', node, refuri=url - ) + node = nodes.reference("", "", node, refuri=url) return [node], [] return _doxylink_handler - - def setup(app): + app.add_css_file("custom.css") - app.add_css_file('custom.css') - - app.add_directive('flagtable', EnumTable) - app.add_directive('enumtable', EnumTable) - app.add_directive('pyinclude', PyInclude) + app.add_directive("flagtable", EnumTable) + app.add_directive("enumtable", EnumTable) + app.add_directive("pyinclude", PyInclude) - skip = os.environ.get('PYAV_SKIP_DOXYLINK') + skip = os.environ.get("PYAV_SKIP_DOXYLINK") for role, (filename, url_base) in doxylink.items(): if skip: app.add_role(role, lambda *args: ([], [])) else: app.add_role(role, doxylink_create_handler(app, filename, url_base)) - - diff --git a/docs/development/hacking.rst b/docs/development/hacking.rst deleted file mode 100644 index 72e61e963..000000000 --- a/docs/development/hacking.rst +++ /dev/null @@ -1,6 +0,0 @@ - -.. It is all in the other file (that we want at the top-level of the repo). - -.. _hacking: - -.. include:: ../../HACKING.rst diff --git a/docs/generate-tagfile b/docs/generate-tagfile index f00ed7ad4..1f729de5c 100755 --- a/docs/generate-tagfile +++ b/docs/generate-tagfile @@ -1,46 +1,23 @@ #!/usr/bin/env python +import argparse import os import subprocess -import argparse parser = argparse.ArgumentParser() -parser.add_argument('-l', '--library', default=os.environ.get('PYAV_LIBRARY')) -parser.add_argument('-o', '--output', default=os.path.abspath(os.path.join( - __file__, - '..', - '_build', - 'doxygen', - 'tagfile.xml', -))) +parser.add_argument("-l", "--library", required=True) +parser.add_argument("-o", "--output", required=True) args = parser.parse_args() - -if not args.library: - print("Please provide --library or set $PYAV_LIBRARY") - exit(1) - -library = os.path.abspath(os.path.join( - __file__, - '..', '..', - 'vendor', - args.library, -)) - -if not os.path.exists(library): - print("Library does not exist:", library) - exit(2) - - output = os.path.abspath(args.output) outdir = os.path.dirname(output) if not os.path.exists(outdir): os.makedirs(outdir) - -proc = subprocess.Popen(['doxygen', '-'], stdin=subprocess.PIPE, cwd=library) -proc.communicate(''' +proc = subprocess.Popen(["doxygen", "-"], stdin=subprocess.PIPE, cwd=args.library) +proc.communicate( + """ #@INCLUDE = doc/Doxyfile GENERATE_TAGFILE = {} @@ -49,6 +26,7 @@ GENERATE_LATEX = no CASE_SENSE_NAMES = yes INPUT = libavcodec libavdevice libavfilter libavformat libavresample libavutil libswresample libswscale -'''.format(output).encode()) - - +""".format( + output + ).encode() +) diff --git a/docs/index.rst b/docs/index.rst index 386e819bd..a1a6d3dff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,8 +54,8 @@ Basic Demo container = av.open(path_to_video) - for frame in container.decode(video=0): - frame.to_image().save('frame-%04d.jpg' % frame.index) + for index, frame in enumerate(container.decode(video=0)): + frame.to_image().save(f"frame-{index:04d}.jpg") Overview diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 5ccac3ffb..093d9bb1a 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -53,5 +53,5 @@ Until we resolve this issue, you should explicitly call :meth:`.Container.close` .. _FFmpeg: https://ffmpeg.org/ -.. _Gitter: https://gitter.im/PyAV-Org +.. _Gitter: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im .. _GitHub: https://github.com/PyAV-Org/pyav diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 72afe8d52..67b2655d6 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -11,7 +11,7 @@ Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Window pip install av -Currently FFmpeg 5.1.2 is used with the following features enabled for all platforms: +Currently FFmpeg 6.0 is used with the following features enabled for all platforms: - fontconfig - gmp @@ -26,7 +26,6 @@ Currently FFmpeg 5.1.2 is used with the following features enabled for all platf - libopenjpeg - libopus - libspeex -- libtheora - libtwolame - libvorbis - libvpx @@ -78,10 +77,10 @@ and a few other tools in general: - Python's development headers -Mac OS X -^^^^^^^^ +MacOS +^^^^^ -On **Mac OS X**, Homebrew_ saves the day:: +On **MacOS**, Homebrew_ saves the day:: brew install ffmpeg pkg-config @@ -116,7 +115,7 @@ Building from the latest source :: # Get PyAV from GitHub. - git clone git@github.com:PyAV-Org/PyAV.git + git clone https://github.com/PyAV-Org/PyAV.git cd PyAV # Prep a virtualenv. @@ -130,11 +129,8 @@ Building from the latest source # Build PyAV. make - # or - python setup.py build_ext --inplace - -On **Mac OS X** you may have issues with regards to Python expecting gcc but finding clang. Try to export the following before installation:: +On **MacOS** you may have issues with regards to Python expecting gcc but finding clang. Try to export the following before installation:: export ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future diff --git a/examples/basics/parse.py b/examples/basics/parse.py index 42a8af160..f4baaecb7 100644 --- a/examples/basics/parse.py +++ b/examples/basics/parse.py @@ -4,7 +4,6 @@ import av import av.datasets - # We want an H.264 stream in the Annex B byte-stream format. # We haven't exposed bitstream filters yet, so we're gonna use the `ffmpeg` CLI. h264_path = "night-sky.h264" diff --git a/examples/basics/remux.py b/examples/basics/remux.py index feb25cbcd..befc24f8a 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -1,7 +1,6 @@ import av import av.datasets - input_ = av.open(av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4")) output = av.open("remuxed.mkv", "w") diff --git a/examples/basics/save_keyframes.py b/examples/basics/save_keyframes.py index 8d31aa621..bc47376cd 100644 --- a/examples/basics/save_keyframes.py +++ b/examples/basics/save_keyframes.py @@ -1,7 +1,6 @@ import av import av.datasets - content = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") with av.open(content) as container: # Signal that we only want to look at keyframes. diff --git a/examples/basics/thread_type.py b/examples/basics/thread_type.py index 2fa7562c9..966a8c8c0 100644 --- a/examples/basics/thread_type.py +++ b/examples/basics/thread_type.py @@ -3,7 +3,6 @@ import av import av.datasets - print("Decoding with default (slice) threading...") container = av.open( diff --git a/examples/numpy/barcode.py b/examples/numpy/barcode.py index 2a3dfff0d..bd40f17a3 100644 --- a/examples/numpy/barcode.py +++ b/examples/numpy/barcode.py @@ -1,10 +1,9 @@ -from PIL import Image import numpy as np +from PIL import Image import av import av.datasets - container = av.open( av.datasets.curated("pexels/time-lapse-video-of-sunset-by-the-sea-854400.mp4") ) diff --git a/examples/numpy/generate_video.py b/examples/numpy/generate_video.py index 250a0bcc1..8b490ea1f 100644 --- a/examples/numpy/generate_video.py +++ b/examples/numpy/generate_video.py @@ -2,7 +2,6 @@ import av - duration = 4 fps = 24 total_frames = duration * fps diff --git a/examples/numpy/generate_video_with_pts.py b/examples/numpy/generate_video_with_pts.py index c570a07d2..d84b34817 100644 --- a/examples/numpy/generate_video_with_pts.py +++ b/examples/numpy/generate_video_with_pts.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 -from fractions import Fraction import colorsys +from fractions import Fraction import numpy as np import av - (width, height) = (640, 360) total_frames = 20 fps = 30 @@ -50,9 +49,9 @@ # draw blocks of a progress bar cx = int(width / total_frames * (frame_i + 0.5)) cy = int(height / 2) - the_canvas[ - cy - block_h2 : cy + block_h2, cx - block_w2 : cx + block_w2 - ] = nice_color + the_canvas[cy - block_h2 : cy + block_h2, cx - block_w2 : cx + block_w2] = ( + nice_color + ) frame = av.VideoFrame.from_ndarray(the_canvas, format="rgb24") diff --git a/flags.txt b/flags.txt deleted file mode 100644 index a2d730711..000000000 --- a/flags.txt +++ /dev/null @@ -1,53 +0,0 @@ - - -Objects with flags -=== -√ AVCodec.capabilities -√ AVCodecDescriptor.props -√ AVCodecContext.flags and flags2 -AVOutputFormat.flags - - - -Thoughts -=== - -- Having both individual properties AND the flags objects is kinda nice. -- I want lowercase flag/enum names, but to also work with the upper ones for b/c. - - -Option: av.enum flags. - - context.flags2 & 'EXPORT_MVS' - - context.flags2 |= 'EXPORT_MVS' - - new APIs: - - 'export_mvs' in context.flags2 - - context.flags2.export_mvs = True - - context.flags2['export_mvs'] = True - -Option: object which represents all flags, but can't work with integer values - - context.flags merges flags and flags2 - - this is really only handy on AVCodecContext, so... fuckit? - -Option: all exposed as individual properties - - context.export_mvs - - - This polutes the attribute space a lot. - - This feels the most "pythonic". - - If you can set multiple in constructors, then NBD if you want to do many. - - I don't like how I have to pick names. - - - - - -How to name -=== - -If a prefix is required, one of: - - is - - has - - can - - use - - do - - diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index bdd73c5fe..49758be4c 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -30,7 +30,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_PROP_BITMAP_SUB AV_CODEC_PROP_TEXT_SUB - #AVCodec.capabilities + # AVCodec.capabilities cdef enum: AV_CODEC_CAP_DRAW_HORIZ_BAND AV_CODEC_CAP_DR1 @@ -90,6 +90,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_PKT_FLAG_KEY AV_PKT_FLAG_CORRUPT + AV_PKT_FLAG_DISCARD + AV_PKT_FLAG_TRUSTED + AV_PKT_FLAG_DISPOSABLE cdef enum: AV_FRAME_FLAG_CORRUPT @@ -131,7 +134,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVClass *priv_class - cdef int av_codec_is_encoder(AVCodec*) cdef int av_codec_is_decoder(AVCodec*) @@ -144,7 +146,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) - cdef struct AVCodecContext: AVClass *av_class @@ -205,9 +206,13 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVPixelFormat pix_fmt AVRational sample_aspect_ratio - int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. + int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. int max_b_frames int has_b_frames + AVColorRange color_range + AVColorPrimaries color_primaries + AVColorTransferCharacteristic color_trc + AVColorSpace colorspace # Audio. AVSampleFormat sample_fmt @@ -261,6 +266,46 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef int AV_NUM_DATA_POINTERS + cdef enum AVPacketSideDataType: + AV_PKT_DATA_PALETTE + AV_PKT_DATA_NEW_EXTRADATA + AV_PKT_DATA_PARAM_CHANGE + AV_PKT_DATA_H263_MB_INFO + AV_PKT_DATA_REPLAYGAIN + AV_PKT_DATA_DISPLAYMATRIX + AV_PKT_DATA_STEREO3D + AV_PKT_DATA_AUDIO_SERVICE_TYPE + AV_PKT_DATA_QUALITY_STATS + AV_PKT_DATA_FALLBACK_TRACK + AV_PKT_DATA_CPB_PROPERTIES + AV_PKT_DATA_SKIP_SAMPLES + AV_PKT_DATA_JP_DUALMONO + AV_PKT_DATA_STRINGS_METADATA + AV_PKT_DATA_SUBTITLE_POSITION + AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL + AV_PKT_DATA_WEBVTT_IDENTIFIER + AV_PKT_DATA_WEBVTT_SETTINGS + AV_PKT_DATA_METADATA_UPDATE + AV_PKT_DATA_MPEGTS_STREAM_ID + AV_PKT_DATA_MASTERING_DISPLAY_METADATA + AV_PKT_DATA_SPHERICAL + AV_PKT_DATA_CONTENT_LIGHT_LEVEL + AV_PKT_DATA_A53_CC + AV_PKT_DATA_ENCRYPTION_INIT_INFO + AV_PKT_DATA_ENCRYPTION_INFO + AV_PKT_DATA_AFD + AV_PKT_DATA_PRFT + AV_PKT_DATA_ICC_PROFILE + AV_PKT_DATA_DOVI_CONF + AV_PKT_DATA_S12M_TIMECODE + AV_PKT_DATA_DYNAMIC_HDR10_PLUS + AV_PKT_DATA_NB + + cdef struct AVPacketSideData: + uint8_t *data; + size_t size; + AVPacketSideDataType type; + cdef enum AVFrameSideDataType: AV_FRAME_DATA_PANSCAN AV_FRAME_DATA_A53_CC @@ -290,15 +335,15 @@ cdef extern from "libavcodec/avcodec.h" nogil: # See: http://ffmpeg.org/doxygen/trunk/structAVFrame.html cdef struct AVFrame: - uint8_t *data[4]; - int linesize[4]; + uint8_t *data[4] + int linesize[4] uint8_t **extended_data - int format # Should be AVPixelFormat or AVSampleFormat - int key_frame # 0 or 1. + int format # Should be AVPixelFormat or AVSampleFormat + int key_frame # 0 or 1. AVPictureType pict_type - int interlaced_frame # 0 or 1. + int interlaced_frame # 0 or 1. int width int height @@ -306,10 +351,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_side_data AVFrameSideData **side_data - int nb_samples # Audio samples - int sample_rate # Audio Sample rate - int channels # Number of audio channels - int channel_layout # Audio channel_layout + int nb_samples # Audio samples + int sample_rate # Audio Sample rate + int channels # Number of audio channels + int channel_layout # Audio channel_layout int64_t pts int64_t pkt_dts @@ -321,7 +366,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDictionary *metadata int flags int decode_error_flags - + AVColorRange color_range + AVColorPrimaries color_primaries + AVColorTransferCharacteristic color_trc + AVColorSpace colorspace cdef AVFrame* avcodec_alloc_frame() @@ -339,7 +387,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pos - cdef int avcodec_fill_audio_frame( AVFrame *frame, int nb_channels, @@ -369,8 +416,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int w int h int nb_colors - uint8_t *data[4]; - int linesize[4]; + uint8_t *data[4] + int linesize[4] AVSubtitleType type char *text char *ass @@ -404,7 +451,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef void avcodec_flush_buffers(AVCodecContext *ctx) - # TODO: avcodec_default_get_buffer is deprecated for avcodec_default_get_buffer2 in newer versions of FFmpeg + # TODO: avcodec_default_get_buffer is deprecated for avcodec_default_get_buffer2 in newer versions of FFmpeg cdef int avcodec_default_get_buffer(AVCodecContext *ctx, AVFrame *frame) cdef void avcodec_default_release_buffer(AVCodecContext *ctx, AVFrame *frame) @@ -442,7 +489,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: ) cdef void av_parser_close(AVCodecParserContext *s) - cdef struct AVCodecParameters: AVMediaType codec_type AVCodecID codec_id diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index e1fd42f45..dd3e91ddf 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -45,7 +45,7 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) - cdef struct AVFilterLink # Defined later. + cdef struct AVFilterLink # Defined later. cdef struct AVFilterContext: diff --git a/include/libavfilter/avfiltergraph.pxd b/include/libavfilter/avfiltergraph.pxd index db9717a50..b773063f9 100644 --- a/include/libavfilter/avfiltergraph.pxd +++ b/include/libavfilter/avfiltergraph.pxd @@ -11,7 +11,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: int pad_idx AVFilterInOut *next - cdef AVFilterGraph* avfilter_graph_alloc() cdef void avfilter_graph_free(AVFilterGraph **ptr) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 06029d9f9..224e76b4e 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -16,7 +16,6 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int AVSEEK_FLAG_ANY cdef int AVSEEK_FLAG_FRAME - cdef int AVIO_FLAG_WRITE cdef enum AVMediaType: @@ -48,6 +47,10 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational r_frame_rate AVRational sample_aspect_ratio + int nb_side_data + AVPacketSideData *side_data + + # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer @@ -207,10 +210,10 @@ cdef extern from "libavformat/avformat.h" nogil: # .. seealso:: FFmpeg's docs: :ffmpeg:`avformat_open_input` # cdef int avformat_open_input( - AVFormatContext **ctx, # NULL will allocate for you. + AVFormatContext **ctx, # NULL will allocate for you. char *filename, - AVInputFormat *format, # Can be NULL. - AVDictionary **options # Can be NULL. + AVInputFormat *format, # Can be NULL. + AVDictionary **options # Can be NULL. ) cdef int avformat_close_input(AVFormatContext **ctx) @@ -224,7 +227,7 @@ cdef extern from "libavformat/avformat.h" nogil: # cdef int avformat_write_header( AVFormatContext *ctx, - AVDictionary **options # Can be NULL + AVDictionary **options # Can be NULL ) cdef int av_write_trailer(AVFormatContext *ctx) @@ -269,7 +272,7 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int avformat_find_stream_info( AVFormatContext *ctx, - AVDictionary **options, # Can be NULL. + AVDictionary **options, # Can be NULL. ) cdef AVStream* avformat_new_stream( diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 50e6bfffd..f9af7a7b0 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -1,9 +1,12 @@ -from libc.stdint cimport int64_t, uint8_t, uint64_t +from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t cdef extern from "libavutil/mathematics.h" nogil: pass +cdef extern from "libavutil/display.h" nogil: + cdef double av_display_rotation_get(const int32_t matrix[9]) + cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) @@ -39,6 +42,68 @@ cdef extern from "libavutil/avutil.h" nogil: # This is nice, but only in FFMpeg: # AV_ROUND_PASS_MINMAX + cdef enum AVColorSpace: + AVCOL_SPC_RGB + AVCOL_SPC_BT709 + AVCOL_SPC_UNSPECIFIED + AVCOL_SPC_RESERVED + AVCOL_SPC_FCC + AVCOL_SPC_BT470BG + AVCOL_SPC_SMPTE170M + AVCOL_SPC_SMPTE240M + AVCOL_SPC_YCOCG + AVCOL_SPC_BT2020_NCL + AVCOL_SPC_BT2020_CL + AVCOL_SPC_NB + + cdef enum AVColorRange: + AVCOL_RANGE_UNSPECIFIED + AVCOL_RANGE_MPEG + AVCOL_RANGE_JPEG + AVCOL_RANGE_NB + + cdef enum AVColorPrimaries: + AVCOL_PRI_RESERVED0 + AVCOL_PRI_BT709 + AVCOL_PRI_UNSPECIFIED + AVCOL_PRI_RESERVED + AVCOL_PRI_BT470M + AVCOL_PRI_BT470BG + AVCOL_PRI_SMPTE170M + AVCOL_PRI_SMPTE240M + AVCOL_PRI_FILM + AVCOL_PRI_BT2020 + AVCOL_PRI_SMPTE428 + AVCOL_PRI_SMPTEST428_1 + AVCOL_PRI_SMPTE431 + AVCOL_PRI_SMPTE432 + AVCOL_PRI_EBU3213 + AVCOL_PRI_JEDEC_P22 + AVCOL_PRI_NB + + cdef enum AVColorTransferCharacteristic: + AVCOL_TRC_RESERVED0 + AVCOL_TRC_BT709 + AVCOL_TRC_UNSPECIFIED + AVCOL_TRC_RESERVED + AVCOL_TRC_GAMMA22 + AVCOL_TRC_GAMMA28 + AVCOL_TRC_SMPTE170M + AVCOL_TRC_SMPTE240M + AVCOL_TRC_LINEAR + AVCOL_TRC_LOG + AVCOL_TRC_LOG_SQRT + AVCOL_TRC_IEC61966_2_4 + AVCOL_TRC_BT1361_ECG + AVCOL_TRC_IEC61966_2_1 + AVCOL_TRC_BT2020_10 + AVCOL_TRC_BT2020_12 + AVCOL_TRC_SMPTE2084 + AVCOL_TRC_SMPTEST2084 + AVCOL_TRC_SMPTE428 + AVCOL_TRC_SMPTEST428_1 + AVCOL_TRC_ARIB_STD_B67 + AVCOL_TRC_NB cdef double M_PI @@ -67,9 +132,9 @@ cdef extern from "libavutil/avutil.h" nogil: # Rescales from one time base to another cdef int64_t av_rescale_q( - int64_t a, # time stamp - AVRational bq, # source time base - AVRational cq # target time base + int64_t a, # time stamp + AVRational bq, # source time base + AVRational cq # target time base ) # Rescale a 64-bit integer with specified rounding. @@ -78,14 +143,14 @@ cdef extern from "libavutil/avutil.h" nogil: int64_t a, int64_t b, int64_t c, - int r # should be AVRounding, but then we can't use bitwise logic. + int r # should be AVRounding, but then we can't use bitwise logic. ) cdef int64_t av_rescale_q_rnd( int64_t a, AVRational bq, AVRational cq, - int r # should be AVRounding, but then we can't use bitwise logic. + int r # should be AVRounding, but then we can't use bitwise logic. ) cdef int64_t av_rescale( @@ -146,7 +211,6 @@ cdef extern from "libavutil/pixdesc.h" nogil: int av_get_padded_bits_per_pixel(AVPixFmtDescriptor *pixdesc) - cdef extern from "libavutil/channel_layout.h" nogil: # Layouts. @@ -166,8 +230,6 @@ cdef extern from "libavutil/channel_layout.h" nogil: cdef char* av_get_channel_description(uint64_t channel) - - cdef extern from "libavutil/audio_fifo.h" nogil: cdef struct AVAudioFifo: @@ -278,6 +340,18 @@ cdef extern from "libavutil/imgutils.h" nogil: AVPixelFormat pix_fmt, int align ) + cdef int av_image_fill_pointers( + uint8_t *pointers[4], + AVPixelFormat pix_fmt, + int height, + uint8_t *ptr, + const int linesizes[4] + ) + cdef int av_image_fill_linesizes( + int linesizes[4], + AVPixelFormat pix_fmt, + int width, + ) cdef extern from "libavutil/log.h" nogil: diff --git a/include/libavutil/error.pxd b/include/libavutil/error.pxd index 8919c54b7..2122772e3 100644 --- a/include/libavutil/error.pxd +++ b/include/libavutil/error.pxd @@ -4,7 +4,6 @@ cdef extern from "libavutil/error.h" nogil: cdef int ENOMEM cdef int EAGAIN - cdef int AVERROR_BSF_NOT_FOUND cdef int AVERROR_BUG cdef int AVERROR_BUFFER_TOO_SMALL diff --git a/include/libavutil/samplefmt.pxd b/include/libavutil/samplefmt.pxd index 867367ee1..a26c6ecfd 100644 --- a/include/libavutil/samplefmt.pxd +++ b/include/libavutil/samplefmt.pxd @@ -12,7 +12,7 @@ cdef extern from "libavutil/samplefmt.h" nogil: AV_SAMPLE_FMT_S32P AV_SAMPLE_FMT_FLTP AV_SAMPLE_FMT_DBLP - AV_SAMPLE_FMT_NB # Number. + AV_SAMPLE_FMT_NB # Number. # Find by name. cdef AVSampleFormat av_get_sample_fmt(char* name) @@ -43,7 +43,6 @@ cdef extern from "libavutil/samplefmt.h" nogil: int align ) - cdef int av_samples_fill_arrays( uint8_t **audio_data, int *linesize, diff --git a/include/libswresample/swresample.pxd b/include/libswresample/swresample.pxd index d76b777a3..65b8314df 100644 --- a/include/libswresample/swresample.pxd +++ b/include/libswresample/swresample.pxd @@ -7,7 +7,6 @@ cdef extern from "libswresample/swresample.h" nogil: cdef char* swresample_configuration() cdef char* swresample_license() - cdef struct SwrContext: pass @@ -20,7 +19,7 @@ cdef extern from "libswresample/swresample.h" nogil: AVSampleFormat in_sample_fmt, int in_sample_rate, int log_offset, - void *log_ctx #logging context, can be NULL + void *log_ctx # logging context, can be NULL ) cdef int swr_convert( diff --git a/pyproject.toml b/pyproject.toml index e9f294dd4..267002b31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,7 @@ [build-system] -requires = ["setuptools", "wheel", "cython"] +requires = ["setuptools", "cython"] + +[tool.isort] +profile = "black" +known_first_party = ["av"] +skip = ["av/__init__.py"] diff --git a/scripts/build-debug-python b/scripts/build-debug-python deleted file mode 100755 index 757946f14..000000000 --- a/scripts/build-debug-python +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" - -export PYAV_PYTHON_VERSION="2.7.13" -export PYAV_PLATFORM_SLUG="$(uname -s).$(uname -r)" -export PYAV_VENV_NAME="$PYAV_PLATFORM_SLUG.cpython-$PYAV_PYTHON_VERSION-debug" -export PYAV_VENV="$PYAV_ROOT/venvs/$PYAV_VENV_NAME" - - -export PYAV_PYTHON_SRC="$PYAV_ROOT/vendor/Python-$PYAV_PYTHON_VERSION" - -if [[ ! -d "$PYAV_PYTHON_SRC" ]]; then - url="https://www.python.org/ftp/python/$PYAV_PYTHON_VERSION/Python-$PYAV_PYTHON_VERSION.tgz" - echo "Downloading $url" - wget -O "$PYAV_PYTHON_SRC.tgz" "$url" || exit 2 - tar -C "$PYAV_ROOT/vendor" -xvzf "$PYAV_PYTHON_SRC.tgz" || exit 3 -fi - -cd "$PYAV_PYTHON_SRC" || exit 4 - -# TODO: Make generic. -export CPPFLAGS="-I$(brew --prefix openssl)/include" -export LDFLAGS="-L$(brew --prefix openssl)/lib" - -# --with-pymalloc \ -./configure \ - --with-pydebug \ - --prefix "$PYAV_VENV" \ - || exit 5 - -make -j 12 || exit 6 - diff --git a/scripts/clean-branches b/scripts/clean-branches deleted file mode 100755 index 83f82f6fb..000000000 --- a/scripts/clean-branches +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python - -import subprocess - - -# These are remote branches that got rebased or something, but I don't have -# control over. -ignored_hashes = set(''' - 078daa1148a84849ea0890b388353acd7333fcea - 0832d2bdaa048fca1393f3d3810b8b8810535f9f - 08e86996736d77df7d694d0a67a126fc7eac7e94 - 097631535fa4cdb87204980541d3f9bb9c7a9ffb - 16ce34ba1d1f1ed4327326a10565f1a9f07107b0 - 183cf1447571aa48baf0665d17d41be1e48f2cc6 - 245a4dca69cf0fff674c6ad5bcfb7929c7347bf6 - 28b4b3988981471ff173679db3643418ff3f5aaa - 3977b5d5be22922f2eb4288e2682f1de8fad8e12 - 5b9f192165855942918f9bd957c30e918b97cbeb - 6091e89de0ae4aff2ba76d21b1110409ef174b78 - 636afe3f0b5b07233edae8e333db35c044c36b30 - 74f79ef74ec281f5e0da51bcfd0b1051aa53edbf - 7737ef6e9e7307c40f326e61cc9291047540bc49 - 8618940d333f44ff960d561dda34167d4dbb81d4 - a2fb55e97788809b5f33b1b0c9241fc77312f606 - aa044b3f62a6d7bf4dde18cba91b1d0dd8a0816a - aa7d01ba458025ede1757e56b638002375bb864a - aafe064e209b667f565c4f57a94b098474d0b184 - afac2d8f89673c012d1f4b845b006911f55d1d86 - b115786b950c87ef9c422752e014297903bca393 - b737c6ceb6750d00f62dfdaa40fee3e757c680a3 - b7bf427a485736e6e1c71605bdce101214bae09f - ba02afa7ea160328b5a3be111c7e276fb9d3c961 - bc5ffe456345286a64ce33ffe5ce6a2ee8b63f40 - c45a337fe49875b1cc28a0501a704890be444765 - c6b1a5ac03e775ea46bffac7bbfea9d73cd03b87 - c9c0d63b09c450d494fba1c4073fbe18851dfaff - cc270d6790c02e6c5e93313d1e6499ce534350b9 - cdd8e4c085a55e258bd551f7bcf4fee60474aa05 - eac71881c24d42f801e9c18e448855a402333960 - efd12926b1f446c32f5a239c0b2d351fa2d78101 - f04dce0e80b4f290482eba4fb3c3ec68f353bf01 - f0d1e82dee788085cf4afad7656a90966e40f7a0 - f518f6e7bf47e00fe0c73a5098ae40813920400f - f779c4371fdace76ee572053b4acb3999ffd4107 -'''.strip().split()) - - -def get_branches(*args): - cmd = ['git', 'branch', '-v', '--abbrev=40'] - cmd.extend(args) - res = {} - for line in subprocess.check_output(cmd).decode().splitlines(): - parts = line[2:].strip().split() - name = parts[0] - hash_ = parts[1] - res[name] = hash_ - return res - -def rm(*args): - subprocess.check_call(('git', 'branch', '-D') + args) - - -# Clean up everything that was merged -for line in subprocess.check_output(['git', 'branch', '--merged']).decode().splitlines(): - line = line.strip() - if not line: - continue - parts = line.split() - if parts[0] == '*': - continue - if parts[-1] in ('develop', 'master'): - continue - rm(parts[-1]) - -for line in subprocess.check_output(['git', 'branch', '-r', '--merged']).decode().splitlines(): - name = line.strip().split()[-1] - if not name: - continue - if name.split('/', 1)[-1] in ('develop', 'master'): - continue - rm('-r', name) - - -our_branches = get_branches() -for name, hash_ in get_branches('-r').items(): - - if hash_ in ignored_hashes: - print("Removing ignored", name) - rm('-r', name) - continue - - if name.startswith('origin/'): - our_branches[name] = hash_ - - -for name in get_branches('-r', '--merged'): - if name.startswith('origin/'): - continue - print("Removing merged", name) - rm('-r', name) - -for name, hash_ in get_branches('-r', '--no-merged').items(): - - remote, branch = name.split('/', 1) - if remote == 'origin': - continue - - for prefix in '', 'origin/': - our_name = prefix + branch - if our_branches.get(our_name) == hash_: - print("Removing identical", name) - rm('-r', name) - break - -# Anything that doesn't root at the same place as us. -for name in get_branches('-r', '--no-contains', 'e105c0b4e64a0471f3f5375a86342c33cb942e23'): - rm('-r', name) - - - diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 3ea3a0c6d..9ea329eb3 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -39,11 +39,10 @@ def get_platform(): with open(args.config_file, "r") as fp: config = json.load(fp) -# create fresh destination directory +# ensure destination directory exists logging.info("Creating directory %s" % args.destination_dir) -if os.path.exists(args.destination_dir): - shutil.rmtree(args.destination_dir) -os.makedirs(args.destination_dir) +if not os.path.exists(args.destination_dir): + os.makedirs(args.destination_dir) for url_template in config["urls"]: tarball_url = url_template.replace("{platform}", get_platform()) diff --git a/scripts/fetch-vendor.json b/scripts/ffmpeg-5.0.json similarity index 61% rename from scripts/fetch-vendor.json rename to scripts/ffmpeg-5.0.json index 2a02f285b..41969df2c 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/ffmpeg-5.0.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] } diff --git a/scripts/ffmpeg-5.1.json b/scripts/ffmpeg-5.1.json new file mode 100644 index 000000000..75e4b8eca --- /dev/null +++ b/scripts/ffmpeg-5.1.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/scripts/ffmpeg-6.0.json b/scripts/ffmpeg-6.0.json new file mode 100644 index 000000000..0eb2034ee --- /dev/null +++ b/scripts/ffmpeg-6.0.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json new file mode 100644 index 000000000..f1585521e --- /dev/null +++ b/scripts/ffmpeg-6.1.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.0-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/scripts/test b/scripts/test index 806011859..01991f5a6 100755 --- a/scripts/test +++ b/scripts/test @@ -18,33 +18,10 @@ istest() { return $? } -if istest black; then - $PYAV_PYTHON -m black --check av examples tests -fi - -if istest flake8; then - # Settings are in setup.cfg - $PYAV_PYTHON -m flake8 av examples tests -fi - -if istest isort; then - # More settings in setup.cfg - $PYAV_PYTHON -m isort --check-only --diff av examples tests -fi - if istest main; then $PYAV_PYTHON setup.py test fi -if istest sdist; then - $PYAV_PYTHON setup.py build_ext - $PYAV_PYTHON setup.py sdist -fi - -if istest doctest; then - make -C docs test -fi - if istest examples; then for name in $(find examples -name '*.py'); do echo diff --git a/scripts/vagrant-test b/scripts/vagrant-test deleted file mode 100755 index dbdab607a..000000000 --- a/scripts/vagrant-test +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -cd /vagrant - -./scripts/build-deps -./scripts/build -./scripts/test diff --git a/setup.cfg b/setup.cfg index 1de247b85..2b4cedb6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,23 +3,3 @@ filename = *.py,*.pyx,*.pxd ignore = E203,W503 max-line-length = 142 per-file-ignores = __init__.py: E402,F401 *.pyx,*.pxd: E211,E225,E227,E402,E999 - -[isort] -default_section = THIRDPARTY -from_first = 1 -known_first_party = av -line_length = 88 -lines_after_imports = 2 -multi_line_output = 3 -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -skip = av/__init__.py - -[metadata] -license = BSD -long_description = file: README.md -long_description_content_type = text/markdown -project_urls = - Bug Reports = https://github.com/PyAV-Org/PyAV/issues - Documentation = https://pyav.org/docs - Feedstock = https://github.com/conda-forge/av-feedstock - Download = https://pypi.org/project/av diff --git a/setup.py b/setup.py index b9e9ba2d4..59239c10a 100644 --- a/setup.py +++ b/setup.py @@ -179,13 +179,25 @@ def parse_cflags(raw_flags): exec(fp.read(), about) package_folders = pathlib.Path("av").glob("**/") -package_data = {".".join(pckg.parts): ["*.pxd"] for pckg in package_folders} +package_data = {".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders} +with open("README.md") as f: + long_description = f.read() + setup( name="av", version=about["__version__"], description="Pythonic bindings for FFmpeg's libraries.", + long_description=long_description, + long_description_content_type="text/markdown", + license="BSD", + project_urls={ + "Bug Reports": "https://github.com/PyAV-Org/PyAV/issues", + "Documentation": "https://pyav.org/docs", + "Feedstock": "https://github.com/conda-forge/av-feedstock", + "Download": "https://pypi.org/project/av", + }, author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", @@ -196,9 +208,7 @@ def parse_cflags(raw_flags): ext_modules=ext_modules, test_suite="tests", entry_points={ - "console_scripts": [ - "pyav = av.__main__:main", - ], + "console_scripts": ["pyav = av.__main__:main"], }, classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/tests/common.py b/tests/common.py index 5b12ca0d9..e2b5f4e16 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,13 +1,12 @@ -from unittest import TestCase as _Base import datetime import errno import functools import os import types +from unittest import TestCase as _Base from av.datasets import fate as fate_suite - try: import PIL.Image as Image import PIL.ImageFilter as ImageFilter diff --git a/tests/requirements.txt b/tests/requirements.txt index 2a321a28d..71a7d58fb 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,8 @@ autopep8 -Cython -editorconfig +black +cython flake8 isort numpy -Pillow -sphinx < 4.4 +pillow +sphinx==5.1.0 diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 30862f2bb..0cbb4acc4 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -32,6 +32,13 @@ def test_data(self): def test_pts_simple(self): fifo = av.AudioFifo() + # ensure __repr__ does not crash + self.assertTrue( + str(fifo).startswith( + " at 0x" + ) + ) + oframe = fifo.read(512) self.assertTrue(oframe is not None) self.assertEqual(oframe.pts, 0) diff --git a/tests/test_audioformat.py b/tests/test_audioformat.py index 5d4eb7871..5334b37d6 100644 --- a/tests/test_audioformat.py +++ b/tests/test_audioformat.py @@ -4,7 +4,6 @@ from .common import TestCase - postfix = "le" if sys.byteorder == "little" else "be" diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 9b66968c1..2a9ae25c0 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -113,7 +113,7 @@ def test_pts_assertion_same_rate(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_assertion_new_rate(self): + def test_pts_assertion_new_rate_up(self): resampler = AudioResampler("s16", "mono", 44100) # resample one frame @@ -131,15 +131,107 @@ def test_pts_assertion_new_rate(self): self.assertEqual(oframe.sample_rate, 44100) self.assertEqual(oframe.samples, 925) + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = "1/48000" + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 925) + self.assertEqual(oframe.time_base, Fraction(1, 44100)) + self.assertEqual(oframe.sample_rate, 44100) + self.assertEqual(oframe.samples, 941) + # flush oframes = resampler.resample(None) self.assertEqual(len(oframes), 1) oframe = oframes[0] - self.assertEqual(oframe.pts, 925) + self.assertEqual(oframe.pts, 941 + 925) self.assertEqual(oframe.time_base, Fraction(1, 44100)) self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 16) + self.assertEqual(oframe.samples, 15) + + def test_pts_assertion_new_rate_down(self): + resampler = AudioResampler("s16", "mono", 48000) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = "1/44100" + iframe.pts = 0 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 0) + self.assertEqual(oframe.time_base, Fraction(1, 48000)) + self.assertEqual(oframe.sample_rate, 48000) + self.assertEqual(oframe.samples, 1098) + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = "1/44100" + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 1098) + self.assertEqual(oframe.time_base, Fraction(1, 48000)) + self.assertEqual(oframe.sample_rate, 48000) + self.assertEqual(oframe.samples, 1114) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 1114 + 1098) + self.assertEqual(oframe.time_base, Fraction(1, 48000)) + self.assertEqual(oframe.sample_rate, 48000) + self.assertEqual(oframe.samples, 18) + + def test_pts_assertion_new_rate_fltp(self): + resampler = AudioResampler("fltp", "mono", 8000, 1024) + + # resample one frame + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = "1/1000" + iframe.pts = 0 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 0) + self.assertEqual(oframe.time_base, Fraction(1, 8000)) + self.assertEqual(oframe.sample_rate, 8000) + self.assertEqual(oframe.samples, 1024) + + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = "1/1000" + iframe.pts = 8192 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 65536) + self.assertEqual(oframe.time_base, Fraction(1, 8000)) + self.assertEqual(oframe.sample_rate, 8000) + self.assertEqual(oframe.samples, 1024) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) def test_pts_missing_time_base(self): resampler = AudioResampler("s16", "mono", 44100) diff --git a/tests/test_codec.py b/tests/test_codec.py index b7af5eedf..98b059fbf 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -5,7 +5,6 @@ from .common import TestCase - # some older ffmpeg versions have no native opus encoder try: opus_c = Codec("opus", "w") @@ -30,6 +29,7 @@ def test_codec_mpeg4_decoder(self): self.assertIn(c.id, (12, 13)) self.assertTrue(c.is_decoder) self.assertFalse(c.is_encoder) + self.assertTrue(c.delay) # audio self.assertIsNone(c.audio_formats) @@ -51,6 +51,7 @@ def test_codec_mpeg4_encoder(self): self.assertIn(c.id, (12, 13)) self.assertTrue(c.is_encoder) self.assertFalse(c.is_decoder) + self.assertTrue(c.delay) # audio self.assertIsNone(c.audio_formats) @@ -72,6 +73,7 @@ def test_codec_opus_decoder(self): self.assertEqual(c.type, "audio") self.assertTrue(c.is_decoder) self.assertFalse(c.is_encoder) + self.assertTrue(c.delay) # audio self.assertIsNone(c.audio_formats) @@ -89,6 +91,7 @@ def test_codec_opus_encoder(self): self.assertEqual(c.type, "audio") self.assertTrue(c.is_encoder) self.assertFalse(c.is_decoder) + self.assertTrue(c.delay) # audio formats = c.audio_formats diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index ee9193c81..0be4ed621 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,12 +1,12 @@ -from fractions import Fraction -from unittest import SkipTest import os import warnings +from fractions import Fraction +from unittest import SkipTest +import av from av import AudioResampler, Codec, Packet from av.codec.codec import UnknownCodecError from av.video.frame import PictureType -import av from .common import TestCase, fate_suite @@ -44,6 +44,12 @@ def test_skip_frame_default(self): ctx = Codec("png", "w").create() self.assertEqual(ctx.skip_frame.name, "DEFAULT") + def test_codec_delay(self): + with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: + self.assertEqual(container.streams.audio[0].codec_context.delay, 312) + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + self.assertEqual(container.streams.video[0].codec_context.delay, 0) + def test_codec_tag(self): ctx = Codec("mpeg4", "w").create() self.assertEqual(ctx.codec_tag, "\x00\x00\x00\x00") @@ -80,6 +86,27 @@ def test_decoder_extradata(self): self.assertEqual(ctx.extradata, None) self.assertEqual(ctx.extradata_size, 0) + def test_decoder_gop_size(self): + ctx = av.codec.Codec("h264", "r").create() + + with warnings.catch_warnings(record=True) as captured: + self.assertIsInstance(ctx.gop_size, int) + self.assertEqual( + captured[0].message.args[0], + "Using VideoCodecContext.gop_size for decoders is deprecated.", + ) + + def test_frame_index(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + stream = container.streams[0] + for frame in container.decode(stream): + with warnings.catch_warnings(record=True) as captured: + self.assertIsInstance(frame.index, int) + self.assertEqual( + captured[0].message.args[0], + "Using `frame.index` is deprecated.", + ) + def test_decoder_timebase(self): ctx = av.codec.Codec("h264", "r").create() @@ -257,6 +284,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): height = options.pop("height", 480) max_frames = options.pop("max_frames", 50) time_base = options.pop("time_base", video_stream.time_base) + gop_size = options.pop("gop_size", 20) ctx = codec.create() ctx.width = width @@ -264,6 +292,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ctx.time_base = time_base ctx.framerate = 1 / ctx.time_base ctx.pix_fmt = pix_fmt + ctx.gop_size = gop_size ctx.options = options # TODO if codec_tag: ctx.codec_tag = codec_tag @@ -299,15 +328,34 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ctx = av.Codec(dec_codec_name, "r").create() ctx.open() + keyframe_indices = [] decoded_frame_count = 0 for frame in iter_raw_frames(path, packet_sizes, ctx): decoded_frame_count += 1 self.assertEqual(frame.width, width) self.assertEqual(frame.height, height) self.assertEqual(frame.format.name, pix_fmt) + if frame.key_frame: + keyframe_indices.append(decoded_frame_count) self.assertEqual(frame_count, decoded_frame_count) + self.assertIsInstance( + all(keyframe_index for keyframe_index in keyframe_indices), int + ) + decoded_gop_sizes = [ + j - i for i, j in zip(keyframe_indices[:-1], keyframe_indices[1:]) + ] + if codec_name in ("dvvideo", "dnxhd") and all( + i == 1 for i in decoded_gop_sizes + ): + raise SkipTest() + for i in decoded_gop_sizes: + self.assertEqual(i, gop_size) + + final_gop_size = decoded_frame_count - max(keyframe_indices) + self.assertLessEqual(final_gop_size, gop_size) + def test_encoding_pcm_s24le(self): self.audio_encoding("pcm_s24le") @@ -317,6 +365,8 @@ def test_encoding_aac(self): def test_encoding_mp2(self): self.audio_encoding("mp2") + maxDiff = None + def audio_encoding(self, codec_name): try: codec = Codec(codec_name, "w") @@ -350,17 +400,123 @@ def audio_encoding(self, codec_name): samples = 0 packet_sizes = [] + pts_expected = [ + 0, + 1098, + 2212, + 3327, + 4441, + 5556, + 6670, + 7785, + 8900, + 10014, + 11129, + 12243, + 13358, + 14472, + 15587, + 16701, + 17816, + 18931, + 20045, + 21160, + 22274, + 23389, + 24503, + 25618, + 26732, + 27847, + 28962, + 30076, + 31191, + 32305, + 33420, + 34534, + 35649, + 36763, + 37878, + 38993, + 40107, + 41222, + 42336, + 43451, + 44565, + 45680, + 46795, + 47909, + 49024, + 50138, + 51253, + 52367, + 53482, + 54596, + 55711, + 56826, + 57940, + 59055, + 60169, + 61284, + 62398, + 63513, + 64627, + 65742, + 66857, + 67971, + 69086, + 70200, + 71315, + 72429, + 73544, + 74658, + 75773, + 76888, + 78002, + 79117, + 80231, + 81346, + 82460, + 83575, + 84689, + 85804, + 86919, + 88033, + 89148, + 90262, + 91377, + 92491, + 93606, + 94720, + 95835, + 96950, + 98064, + 99179, + 100293, + 101408, + ] + if codec_name == "aac": + pts_expected_encoded = list((-1024 + n * 1024 for n in range(101))) + elif codec_name == "mp2": + pts_expected_encoded = list((-481 + n * 1152 for n in range(89))) + else: + pts_expected_encoded = pts_expected.copy() with open(path, "wb") as f: for frame in iter_frames(container, audio_stream): resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: + self.assertEqual(resampled_frame.pts, pts_expected.pop(0)) + self.assertEqual(resampled_frame.time_base, Fraction(1, 48000)) samples += resampled_frame.samples for packet in ctx.encode(resampled_frame): + self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) + self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) for packet in ctx.encode(None): + self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) + self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py new file mode 100644 index 000000000..99c9f2fa2 --- /dev/null +++ b/tests/test_colorspace.py @@ -0,0 +1,39 @@ +import av +from av.video.reformatter import ColorRange, Colorspace + +from .common import TestCase, fate_suite + + +class TestColorSpace(TestCase): + def test_penguin_joke(self): + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + stream = container.streams.video[0] + + self.assertEqual(stream.codec_context.color_range, 2) + self.assertEqual(stream.codec_context.color_range, ColorRange.JPEG) + + self.assertEqual(stream.codec_context.color_primaries, 2) + self.assertEqual(stream.codec_context.color_trc, 2) + + self.assertEqual(stream.codec_context.colorspace, 5) + self.assertEqual(stream.codec_context.colorspace, Colorspace.ITU601) + + for packet in container.demux(stream): + for frame in packet.decode(): + self.assertEqual(frame.color_range, ColorRange.JPEG) # a.k.a "pc" + self.assertEqual(frame.colorspace, Colorspace.ITU601) + return + + def test_sky_timelapse(self): + container = av.open( + av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + ) + stream = container.streams.video[0] + + self.assertEqual(stream.codec_context.color_range, 1) + self.assertEqual(stream.codec_context.color_range, ColorRange.MPEG) + self.assertEqual(stream.codec_context.color_primaries, 1) + self.assertEqual(stream.codec_context.color_trc, 1) + self.assertEqual(stream.codec_context.colorspace, 1) diff --git a/tests/test_decode.py b/tests/test_decode.py index 564ea24cd..87a84ba12 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -111,3 +111,48 @@ def test_decode_video_corrupt(self): self.assertEqual(packet_count, 1) self.assertEqual(frame_count, 0) + + def test_decode_close_then_use(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + container.close() + + # Check accessing every attribute either works or raises + # an `AssertionError`. + for attr in dir(container): + with self.subTest(attr=attr): + try: + getattr(container, attr) + except AssertionError: + pass + + def test_flush_decoded_video_frame_count(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video_stream = next(s for s in container.streams if s.type == "video") + + self.assertIs(video_stream, container.streams.video[0]) + + # Decode the first GOP, which requires a flush to get all frames + have_keyframe = False + input_count = 0 + output_count = 0 + + for packet in container.demux(video_stream): + if packet.is_keyframe: + if have_keyframe: + break + have_keyframe = True + + input_count += 1 + + for frame in video_stream.decode(packet): + output_count += 1 + + # Check the test works as expected and requires a flush + self.assertLess(output_count, input_count) + + for frame in video_stream.decode(None): + # The Frame._time_base is not set by PyAV + self.assertIsNone(frame.time_base) + output_count += 1 + + self.assertEqual(output_count, input_count) diff --git a/tests/test_doctests.py b/tests/test_doctests.py index 1449355bd..63220889c 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -1,7 +1,7 @@ -from unittest import TestCase import doctest import pkgutil import re +from unittest import TestCase import av import av.datasets diff --git a/tests/test_encode.py b/tests/test_encode.py index a293cbf83..3ca031794 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,15 +1,17 @@ +import io +import math from fractions import Fraction from unittest import SkipTest -import math +import numpy as np + +import av from av import AudioFrame, VideoFrame from av.audio.stream import AudioStream from av.video.stream import VideoStream -import av from .common import Image, TestCase, fate_suite - WIDTH = 320 HEIGHT = 240 DURATION = 48 @@ -54,10 +56,10 @@ def write_rgb_rotate(output): ) frame.planes[0].update(image.tobytes()) - for packet in stream.encode(frame): + for packet in stream.encode_lazy(frame): output.mux(packet) - for packet in stream.encode(None): + for packet in stream.encode_lazy(None): output.mux(packet) @@ -277,6 +279,37 @@ def test_stream_index(self): self.assertIs(apacket.stream, astream) self.assertEqual(apacket.stream_index, 1) + def test_stream_audio_resample(self): + with av.open(self.sandboxed("output.mov"), "w") as output: + vstream = output.add_stream("mpeg4", 24) + vstream.pix_fmt = "yuv420p" + vstream.width = 320 + vstream.height = 240 + + astream = output.add_stream("aac", sample_rate=8000, layout="mono") + frame_size = 512 + + pts_expected = [-1024, 0, 512, 1024, 1536, 2048, 2560] + pts = 0 + for i in range(15): + aframe = AudioFrame("s16", "mono", samples=frame_size) + aframe.sample_rate = 8000 + aframe.time_base = Fraction(1, 1000) + aframe.pts = pts + aframe.dts = pts + pts += 32 + apackets = astream.encode(aframe) + if apackets: + apacket = apackets[0] + self.assertEqual(apacket.pts, pts_expected.pop(0)) + self.assertEqual(apacket.time_base, Fraction(1, 8000)) + + apackets = astream.encode(None) + if apackets: + apacket = apackets[0] + self.assertEqual(apacket.pts, pts_expected.pop(0)) + self.assertEqual(apacket.time_base, Fraction(1, 8000)) + def test_set_id_and_time_base(self): with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") @@ -291,3 +324,84 @@ def test_set_id_and_time_base(self): self.assertEqual(stream.time_base, None) stream.time_base = Fraction(1, 48000) self.assertEqual(stream.time_base, Fraction(1, 48000)) + + +def encode_file_with_max_b_frames(max_b_frames): + """ + Create an encoded video file (or file-like object) with the given + maximum run of B frames. + + max_b_frames: non-negative integer which is the maximum allowed run + of consecutive B frames. + + Returns: a file-like object. + """ + # Create a video file that is entirely arbitrary, but with the passed + # max_b_frames parameter. + file = io.BytesIO() + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream("h264", rate=30) + stream.width = 640 + stream.height = 480 + stream.pix_fmt = "yuv420p" + stream.codec_context.gop_size = 15 + stream.codec_context.max_b_frames = max_b_frames + + for i in range(50): + array = np.empty((stream.height, stream.width, 3), dtype=np.uint8) + # This appears to hit a complexity "sweet spot" that makes the codec + # want to use B frames. + array[:, :] = (i, 0, 255 - i) + frame = av.VideoFrame.from_ndarray(array, format="rgb24") + for packet in stream.encode(frame): + container.mux(packet) + + for packet in stream.encode(): + container.mux(packet) + + container.close() + file.seek(0) + + return file + + +def max_b_frame_run_in_file(file): + """ + Count the maximum run of B frames in a file (or file-like object). + + file: the file or file-like object in which to count the maximum run + of B frames. The file should contain just one video stream. + + Returns: non-negative integer which is the maximum B frame run length. + """ + container = av.open(file) + stream = container.streams.video[0] + + max_b_frame_run = 0 + b_frame_run = 0 + for packet in container.demux(stream): + for frame in packet.decode(): + if frame.pict_type == av.video.frame.PictureType.B: + b_frame_run += 1 + else: + max_b_frame_run = max(max_b_frame_run, b_frame_run) + b_frame_run = 0 + + # Outside chance that the longest run was at the end of the file. + max_b_frame_run = max(max_b_frame_run, b_frame_run) + + container.close() + + return max_b_frame_run + + +class TestMaxBFrameEncoding(TestCase): + def test_max_b_frames(self): + """ + Test that we never get longer runs of B frames than we asked for with + the max_b_frames property. + """ + for max_b_frames in range(4): + file = encode_file_with_max_b_frames(max_b_frames) + actual_max_b_frames = max_b_frame_run_in_file(file) + self.assertTrue(actual_max_b_frames <= max_b_frames) diff --git a/tests/test_enums.py b/tests/test_enums.py index bc8385f5e..39bf856ad 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -4,7 +4,6 @@ from .common import TestCase - # This must be at the top-level. PickleableFooBar = define_enum("PickleableFooBar", __name__, [("FOO", 1)]) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index c67f7fb8e..c3d239c50 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -1,5 +1,5 @@ -from fractions import Fraction import warnings +from fractions import Fraction import av @@ -308,7 +308,6 @@ def test_stream_probing(self): self.assertEqual(stream.display_aspect_ratio, Fraction(4, 3)) self.assertEqual(stream.format.name, "yuv420p") self.assertFalse(stream.has_b_frames) - self.assertEqual(stream.gop_size, 12) self.assertEqual(stream.height, 576) self.assertEqual(stream.max_bit_rate, None) self.assertEqual(stream.sample_aspect_ratio, Fraction(16, 15)) @@ -322,13 +321,13 @@ def test_stream_probing(self): # Deprecated properties. with warnings.catch_warnings(record=True) as captured: - self.assertIsNone(stream.framerate) + stream.framerate self.assertEqual( captured[0].message.args[0], "VideoStream.framerate is deprecated as it is not always set; please use VideoStream.average_rate.", ) with warnings.catch_warnings(record=True) as captured: - self.assertIsNone(stream.rate) + stream.rate self.assertEqual( captured[0].message.args[0], "VideoStream.rate is deprecated as it is not always set; please use VideoStream.average_rate.", @@ -362,7 +361,6 @@ def test_stream_probing(self): self.assertTrue(str(stream).startswith("