diff --git a/AUTHORS.py b/AUTHORS.py new file mode 100644 index 000000000..fd47a713d --- /dev/null +++ b/AUTHORS.py @@ -0,0 +1,103 @@ +import math +import subprocess + + +print('''Contributors +============ + +All contributors (by number of commits): +''') + + +email_map = { + + # Maintainers. + 'git@mikeboers.com': 'github@mikeboers.com', + 'mboers@keypics.com': 'github@mikeboers.com', + 'mikeb@loftysky.com': 'github@mikeboers.com', + 'mikeb@markmedia.co': 'github@mikeboers.com', + 'westernx@mikeboers.com': 'github@mikeboers.com', + + # Junk. + 'mark@mark-VirtualBox.(none)': None, + + # Aliases. + 'a.davoudi@aut.ac.ir': 'davoudialireza@gmail.com', + 'tcaswell@bnl.gov': 'tcaswell@gmail.com', + 'xxr3376@gmail.com': 'xxr@megvii.com', + 'dallan@pha.jhu.edu': 'daniel.b.allan@gmail.com', + '61652821+laggykiller@users.noreply.github.com': 'chaudominic2@gmail.com', + +} + +name_map = { + 'caspervdw@gmail.com': 'Casper van der Wel', + 'daniel.b.allan@gmail.com': 'Dan Allan', + 'mgoacolou@cls.fr': 'Manuel Goacolou', + 'mindmark@gmail.com': 'Mark Reid', + 'moritzkassner@gmail.com': 'Moritz Kassner', + 'vidartf@gmail.com': 'Vidar Tonaas Fauske', + 'xxr@megvii.com': 'Xinran Xu', +} + +github_map = { + 'billy.shambrook@gmail.com': 'billyshambrook', + 'daniel.b.allan@gmail.com': 'danielballan', + 'davoudialireza@gmail.com': 'adavoudi', + 'github@mikeboers.com': 'mikeboers', + 'jeremy.laine@m4x.org': 'jlaine', + 'kalle.litterfeldt@gmail.com': 'litterfeldt', + 'mindmark@gmail.com': 'markreidvfx', + 'moritzkassner@gmail.com': 'mkassner', + 'rush@logic.cz': 'radek-senfeld', + 'self@brendanlong.com': 'brendanlong', + 'tcaswell@gmail.com': 'tacaswell', + 'ulrik.mikaelsson@magine.com': 'rawler', + 'vidartf@gmail.com': 'vidartf', + 'willpatera@gmail.com': 'willpatera', + 'xxr@megvii.com': 'xxr3376', + 'chaudominic2@gmail.com': 'laggykiller', + 'wyattblue@auto-editor.com': 'WyattBlue', +} + + +email_count = {} +for line in subprocess.check_output(['git', 'log', '--format=%aN,%aE']).decode().splitlines(): + name, email = line.strip().rsplit(',', 1) + + email = email_map.get(email, email) + if not email: + continue + + names = name_map.setdefault(email, set()) + if isinstance(names, set): + names.add(name) + + email_count[email] = email_count.get(email, 0) + 1 + + +last = None +block_i = 0 +for email, count in sorted(email_count.items(), key=lambda x: (-x[1], x[0])): + + # This is the natural log, because of course it should be. ;) + order = int(math.log(count)) + if last and last != order: + block_i += 1 + print() + last = order + + names = name_map[email] + if isinstance(names, set): + name = ', '.join(sorted(names)) + else: + name = names + + github = github_map.get(email) + + # The '-' vs '*' is so that Sphinx treats them as different lists, and + # introduces a gap bettween them. + if github: + print('%s %s <%s>; `@%s `_' % ('-*'[block_i % 2], name, email, github, github)) + else: + print('%s %s <%s>' % ('-*'[block_i % 2], name, email, )) diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 000000000..68595a7da --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,92 @@ +Contributors +============ + +All contributors (by number of commits): + +- Mike Boers ; `@mikeboers `_ + +* Jeremy Lainé ; `@jlaine `_ + +- WyattBlue ; `@WyattBlue `_ +- Mark Reid ; `@markreidvfx `_ + +* Vidar Tonaas Fauske ; `@vidartf `_ +* laggykiller ; `@laggykiller `_ +* Billy Shambrook ; `@billyshambrook `_ +* Casper van der Wel +* Philip de Nier +* Tadas Dailyda +* JoeUgly <41972063+JoeUgly@users.noreply.github.com> +* Justin Wong <46082645+uvjustin@users.noreply.github.com> + +- Alba Mendez +- Mark Harfouche +- Xinran Xu ; `@xxr3376 `_ +- Dan Allan ; `@danielballan `_ +- Dave Johansen +- Christoph Rackwitz +- Alireza Davoudi ; `@adavoudi `_ +- Jonathan Drolet +- Moritz Kassner ; `@mkassner `_ +- Santtu Keskinen +- 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> +* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Artturin +* 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 +* Roland van Laar +* Santiago Castro +* Kengo Sawatsu +* FirefoxMetzger +* hyenal +* Brendan Long ; `@brendanlong `_ +* Семён Марьясин +* Stephen.Y +* Tom Flanagan +* Tim O'Shea +* Tim Ahpee +* Jonas Tingeborn +* Pino Toscano +* Ulrik Mikaelsson +* Vasiliy Kotov +* Koichi Akabe +* David Joy +* Sviatoslav Sydorenko (Святослав Сидоренко) diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 000000000..526ce98a6 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,725 @@ +Changelog +========= + +We are operating with `semantic versioning `_. + +.. + Please try to update this file in the commits that make the changes. + + To make merging/rebasing easier, we don't manually break lines in here + when they are too long, so any particular change is just one line. + + To make tracking easier, please add either ``closes #123`` or ``fixes #123`` + to the first line of the commit message. There are more syntaxes at: + . + + Note that they these tags will not actually close the issue/PR until they + are merged into the "default" branch. + + +v12.1.0 +------- + +Features: + +- Build binary wheels with webp support. +- Allow disabling logs, disable logs by default. +- Add bitstream filters by @skeskinen in (:issue:`1375`) (:issue:`1379`). +- Expose CodecContext flush_buffers by @skeskinen in (:issue:`1382`). + +Fixes: + +- Fix type stubs, add missing type stubs. +- Add S12M_TIMECODE by @WyattBlue in (:issue:`1381`). +- Subtitle.text now returns bytes by @WyattBlue in (:issue:`13981). +- Allow packet.duration to be writable by @WyattBlue in (:issue:`1399`). +- Remove deprecated `VideoStream.frame_rate` by @WyattBlue in (:issue:`1351`). +- Build with Arm for PyPy now by @WyattBlue in (:issue:`1395`). +- Fix #1378 by @WyattBlue in (:issue:`1400`). +- setup.py: use PKG_CONFIG env var to get the pkg-config to use by @Artturin in (:issue:`1387`). + +v12.0.0 +------- + +Major: + +- Add type hints. +- Update FFmpeg to 6.1.1 for the binary wheels. +- Update libraries for the binary wheels (notably dav1d to 1.4.1). +- Deprecate VideoCodecContext.gop_size for decoders by @JoeSchiff in (:issue:`1256`). +- Deprecate frame.index by @JoeSchiff in (:issue:`1218`). + +Features: + +- Allow using pathlib.Path for av.open by @WyattBlue in (:issue:`1231`). +- Add `max_b_frames` property to CodecContext by @davidplowman in (:issue:`1119`). +- Add `encode_lazy` method to CodecContext by @rawler in (:issue:`1092`). +- Add `color_range` to CodecContext/Frame by @johanjeppsson in (:issue:`686`). +- Set `time_base` for AudioResampler by @daveisfera in (:issue:`1209`). +- Add support for ffmpeg's AVCodecContext::delay by @JoeSchiff in (:issue:`1279`). +- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by @WyattBlue in (:issue:`1304`). +- Add `bits_per_coded_sample` to VideoCodecContext by @rvanlaar in (:issue:`1203`). +- AssSubtitle.ass now returns as bytes by @WyattBlue in (:issue:`1333`). +- Expose DISPLAYMATRIX side data by @hyenal in (:issue:`1249`). + +Fixes: + +- Convert deprecated Cython extension class properties to decorator syntax by @JoeSchiff +- Check None packet when setting time_base after decode by @philipnbbc in (:issue:`1281`). +- Remove deprecated `Buffer.to_bytes` by @WyattBlue in (:issue:`1286`). +- Remove deprecated `Packet.decode_one` by @WyattBlue in (:issue:`1301`). + +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 +------- + +Major: + +- Add support for FFmpeg 5.0 and 5.1 (:issue:`817`). +- Drop support for FFmpeg < 4.3. +- Deprecate `CodecContext.time_base` for decoders (:issue:`966`). +- Deprecate `VideoStream.framerate` and `VideoStream.rate` (:issue:`1005`). +- Stop proxying `Codec` from `Stream` instances (:issue:`1037`). + +Features: + +- Update FFmpeg to 5.1.2 for the binary wheels. +- Provide binary wheels for Python 3.11 (:issue:`1019`). +- Add VideoFrame ndarray operations for gbrp formats (:issue:`986`). +- Add VideoFrame ndarray operations for gbrpf32 formats (:issue:`1028`). +- Add VideoFrame ndarray operations for nv12 format (:issue:`996`). + +Fixes: + +- Fix conversion to numpy array for multi-byte formats (:issue:`981`). +- Safely iterate over filter pads (:issue:`1000`). + +v9.2.0 +------ + +Features: + +- Update binary wheels to enable libvpx support. +- Add an `io_open` argument to `av.open` for multi-file custom I/O. +- Add support for AV_FRAME_DATA_SEI_UNREGISTERED (:issue:`723`). +- Ship .pxd files to allow other libraries to `cimport av` (:issue:`716`). + +Fixes: + +- Fix an `ImportError` when using Python 3.8/3.9 via Conda (:issue:`952`). +- Fix a muxing memory leak which was introduced in v9.1.0 (:issue:`959`). + +v9.1.1 +------ + +Fixes: + +- Update binary wheels to update dependencies on Windows, disable ALSA on Linux. + +v9.1.0 +------ + +Features: + +- Add VideoFrame ndarray operations for rgb48be, rgb48le, rgb64be, rgb64le pixel formats. +- Add VideoFrame ndarray operations for gray16be, gray16le pixel formats (:issue:`674`). +- Make it possible to use av.open() on a pipe (:issue:`738`). +- Use the "ASS without timings" format when decoding subtitles. + +Fixes: + +- Update binary wheels to fix security vulnerabilities (:issue:`921`) and enable ALSA on Linux (:issue:`941`). +- Fix crash when closing an output container an encountering an I/O error (:issue:`613`). +- Fix crash when probing corrupt raw format files (:issue:`590`). +- Fix crash when manipulating streams with an unknown codec (:issue:`689`). +- Remove obsolete KEEP_SIDE_DATA and MP4A_LATM flags which are gone in FFmpeg 5.0. +- Deprecate `to_bytes()` method of Packet, Plane and SideData, use `bytes(packet)` instead. + +v9.0.2 +------ + +Minor: + +- Update FFmpeg to 4.4.1 for the binary wheels. +- Fix framerate when writing video with FFmpeg 4.4 (:issue:`876`). + +v9.0.1 +------ + +Minor: + +- Update binary wheels to fix security vulnerabilities (:issue:`901`). + +v9.0.0 +------ + +Major: + +- Re-implement AudioResampler with aformat and buffersink (:issue:`761`). + AudioResampler.resample() now returns a list of frames. +- Remove deprecated methods: AudioFrame.to_nd_array, VideoFrame.to_nd_array and Stream.seek. + +Minor: + +- Provide binary wheels for macOS/arm64 and Linux/aarch64. +- Simplify setup.py, require Cython. +- Update the installation instructions in favor of PyPI. +- Fix VideoFrame.to_image with height & width (:issue:`878`). +- Fix setting Stream time_base (:issue:`784`). +- Replace deprecated av_init_packet with av_packet_alloc (:issue:`872`). +- Validate pixel format in VideoCodecContext.pix_fmt setter (:issue:`815`). +- Fix AudioFrame ndarray conversion endianness (:issue:`833`). +- Improve time_base support with filters (:issue:`765`). +- Allow flushing filters by sending `None` (:issue:`886`). +- Avoid unnecessary vsnprintf() calls in log_callback() (:issue:`877`). +- Make Frame.from_ndarray raise ValueError instead of AssertionError. + +v8.1.0 +------ + +Minor: + +- Update FFmpeg to 4.3.2 for the binary wheels. +- Provide binary wheels for Python 3.10 (:issue:`820`). +- Stop providing binary wheels for end-of-life Python 3.6. +- Fix args order in Frame.__repr__ (:issue:`749`). +- Fix documentation to remove unavailable QUIET log level (:issue:`719`). +- Expose codec_context.codec_tag (:issue:`741`). +- Add example for encoding with a custom PTS (:issue:`725`). +- Use av_packet_rescale_ts in Packet._rebase_time() (:issue:`737`). +- Do not hardcode errno values in test suite (:issue:`729`). +- Use av_guess_format for output container format (:issue:`691`). +- Fix setting CodecContext.extradata (:issue:`658`, :issue:`740`). +- Fix documentation code block indentation (:issue:`783`). +- Fix link to Conda installation instructions (:issue:`782`). +- Export AudioStream from av.audio (:issue:`775`). +- Fix setting CodecContext.extradata (:issue:`801`). + +v8.0.3 +------ + +Minor: + +- Update FFmpeg to 4.3.1 for the binary wheels. + +v8.0.2 +------ + +Minor: + +- Enable GnuTLS support in the FFmpeg build used for binary wheels (:issue:`675`). +- Make binary wheels compatible with Mac OS X 10.9+ (:issue:`662`). +- Drop Python 2.x buffer protocol code. +- Remove references to previous repository location. + +v8.0.1 +------ + +Minor: + +- Enable additional FFmpeg features in the binary wheels. +- Use os.fsencode for both input and output file names (:issue:`600`). + +v8.0.0 +------ + +Major: + +- Drop support for Python 2 and Python 3.4. +- Provide binary wheels for Linux, Mac and Windows. + +Minor: + +- Remove shims for obsolete FFmpeg versions (:issue:`588`). +- Add yuvj420p format for :meth:`VideoFrame.from_ndarray` and :meth:`VideoFrame.to_ndarray` (:issue:`583`). +- Add support for palette formats in :meth:`VideoFrame.from_ndarray` and :meth:`VideoFrame.to_ndarray` (:issue:`601`). +- Fix Python 3.8 deprecation warning related to abstract base classes (:issue:`616`). +- Remove ICC profiles from logos (:issue:`622`). + +Fixes: + +- Avoid infinite timeout in :func:`av.open` (:issue:`589`). + +v7.0.1 +------ + +Fixes: + +- Removed deprecated ``AV_FRAME_DATA_QP_TABLE_*`` enums. (:issue:`607`) + + +v7.0.0 +------ + +Major: + +- Drop support for FFmpeg < 4.0. (:issue:`559`) +- Introduce per-error exceptions, and mirror the builtin exception hierarchy. It is recommended to examine your error handling code, as common FFmpeg errors will result in `ValueError` baseclasses now. (:issue:`563`) +- Data stream's `encode` and `decode` return empty lists instead of none allowing common API use patterns with data streams. +- Remove ``whence`` parameter from :meth:`InputContainer.seek` as non-time seeking doesn't seem to actually be supported by any FFmpeg formats. + +Minor: + +- Users can disable the logging system to avoid lockups in sub-interpreters. (:issue:`545`) +- Filters support audio in general, and a new :meth:`.Graph.add_abuffer`. (:issue:`562`) +- :func:`av.open` supports `timeout` parameters. (:issue:`480` and :issue:`316`) +- Expose :attr:`Stream.base_rate` and :attr:`Stream.guessed_rate`. (:issue:`564`) +- :meth:`.VideoFrame.reformat` can specify interpolation. +- Expose many sets of flags. + +Fixes: + +- Fix typing in :meth:`.CodecContext.parse` and make it more robust. +- Fix wrong attribute in ByteSource. (:issue:`340`) +- Remove exception that would break audio remuxing. (:issue:`537`) +- Log messages include last FFmpeg error log in more helpful way. +- Use AVCodecParameters so FFmpeg doesn't complain. (:issue:`222`) + + +v6.2.0 +------ + +Major: + +- Allow :meth:`av.open` to be used as a context manager. +- Fix compatibility with PyPy, the full test suite now passes. (:issue:`130`) + +Minor: + +- Add :meth:`.InputContainer.close` method. (:issue:`317`, :issue:`456`) +- Ensure audio output gets flushes when using a FIFO. (:issue:`511`) +- Make Python I/O buffer size configurable. (:issue:`512`) +- Make :class:`.AudioFrame` and :class:`VideoFrame` more garbage-collector friendly by breaking a reference cycle. (:issue:`517`) + +Build: + +- Do not install the `scratchpad` package. + + +v6.1.2 +------ + +Micro: + +- Fix a numpy deprecation warning in :meth:`.AudioFrame.to_ndarray`. + + +v6.1.1 +------ + +Micro: + +- Fix alignment in :meth:`.VideoFrame.from_ndarray`. (:issue:`478`) +- Fix error message in :meth:`.Buffer.update`. + +Build: + +- Fix more compiler warnings. + + +v6.1.0 +------ + +Minor: + +- ``av.datasets`` for sample data that is pulled from either FFmpeg's FATE suite, or our documentation server. +- :meth:`.InputContainer.seek` gets a ``stream`` argument to specify the ``time_base`` the requested ``offset`` is in. + +Micro: + +- Avoid infinite look in ``Stream.__getattr__``. (:issue:`450`) +- Correctly handle Python I/O with no ``seek`` method. +- Remove ``Datastream.seek`` override (:issue:`299`) + +Build: + +- Assert building against compatible FFmpeg. (:issue:`401`) +- Lock down Cython lanaguage level to avoid build warnings. (:issue:`443`) + +Other: + +- Incremental improvements to docs and tests. +- Examples directory will now always be runnable as-is, and embeded in the docs (in a copy-pastable form). + + +v6.0.0 +------ + +Major: + +- Drop support for FFmpeg < 3.2. +- Remove ``VideoFrame.to_qimage`` method, as it is too tied to PyQt4. (:issue:`424`) + +Minor: + +- Add support for all known sample formats in :meth:`.AudioFrame.to_ndarray` and add :meth:`.AudioFrame.to_ndarray`. (:issue:`422`) +- Add support for more image formats in :meth:`.VideoFrame.to_ndarray` and :meth:`.VideoFrame.from_ndarray`. (:issue:`415`) + +Micro: + +- Fix a memory leak in :meth:`.OutputContainer.mux_one`. (:issue:`431`) +- Ensure :meth:`.OutputContainer.close` is called at destruction. (:issue:`427`) +- Fix a memory leak in :class:`.OutputContainer` initialisation. (:issue:`427`) +- Make all video frames created by PyAV use 8-byte alignment. (:issue:`425`) +- Behave properly in :meth:`.VideoFrame.to_image` and :meth:`.VideoFrame.from_image` when ``width != line_width``. (:issue:`425`) +- Fix manipulations on video frames whose width does not match the line stride. (:issue:`423`) +- Fix several :attr:`.Plane.line_size` misunderstandings. (:issue:`421`) +- Consistently decode dictionary contents. (:issue:`414`) +- Always use send/recv en/decoding mechanism. This removes the ``count`` parameter, which was not used in the send/recv pipeline. (:issue:`413`) +- Remove various deprecated iterators. (:issue:`412`) +- Fix a memory leak when using Python I/O. (:issue:`317`) +- Make :meth:`.OutputContainer.mux_one` call `av_interleaved_write_frame` with the GIL released. + +Build: + +- Remove the "reflection" mechanism, and rely on FFmpeg version we build against to decide which methods to call. (:issue:`416`) +- Fix many more ``const`` warnings. + + +v0.x.y +------ + +.. note:: + + Below here we used ``v0.x.y``. + + We incremented ``x`` to signal a major change (i.e. backwards + incompatibilities) and incremented ``y`` as a minor change (i.e. backwards + compatible features). + + Once we wanted more subtlety and felt we had matured enough, we jumped + past the implications of ``v1.0.0`` straight to ``v6.0.0`` + (as if we had not been stuck in ``v0.x.y`` all along). + + +v0.5.3 +------ + +Minor: + +- Expose :attr:`.VideoFrame.pict_type` as :class:`.PictureType` enum. + (:pr:`402`) +- Expose :attr:`.Codec.video_rates` and :attr:`.Codec.audio_rates`. + (:pr:`381`) + +Patch: + +- Fix :attr:`.Packet.time_base` handling during flush. + (:pr:`398`) +- :meth:`.VideoFrame.reformat` can throw exceptions when requested colorspace + transforms aren't possible. +- Wrapping the stream object used to overwrite the ``pix_fmt`` attribute. + (:pr:`390`) + +Runtime: + +- Deprecate ``VideoFrame.ptr`` in favour of :attr:`VideoFrame.buffer_ptr`. +- Deprecate ``Plane.update_buffer()`` and ``Packet.update_buffer`` in favour of + :meth:`.Plane.update`. + (:pr:`407`) +- Deprecate ``Plane.update_from_string()`` in favour of :meth:`.Plane.update`. + (:pr:`407`) +- Deprecate ``AudioFrame.to_nd_array()`` and ``VideoFrame.to_nd_array()`` in + favour of :meth:`.AudioFrame.to_ndarray` and :meth:`.VideoFrame.to_ndarray`. + (:pr:`404`) + +Build: + +- CI covers more cases, including macOS. + (:pr:`373` and :pr:`399`) +- Fix many compilation warnings. + (:issue:`379`, :pr:`380`, :pr:`387`, and :pr:`388`) + +Docs: + +- Docstrings for many commonly used attributes. + (:pr:`372` and :pr:`409`) + + +v0.5.2 +------ + +Build: + +- Fixed Windows build, which broke in v0.5.1. +- Compiler checks are not cached by default. This behaviour is retained if you + ``source scripts/activate.sh`` to develop PyAV. + (:issue:`256`) +- Changed to ``PYAV_SETUP_REFLECT_DEBUG=1`` from ``PYAV_DEBUG_BUILD=1``. + + +v0.5.1 +------ + +Build: + +- Set ``PYAV_DEBUG_BUILD=1`` to force a verbose reflection (mainly for being + installed via ``pip``, which is why this is worth a release). + + +v0.5.0 +------ + +Major: + +- Dropped support for Libav in general. + (:issue:`110`) +- No longer uses libavresample. + +Minor: + +- ``av.open`` has ``container_options`` and ``stream_options``. +- ``Frame`` includes ``pts`` in ``repr``. + +Patch: + +- EnumItem's hash calculation no longer overflows. + (:issue:`339`, :issue:`341` and :issue:`342`.) +- Frame.time_base was not being set in most cases during decoding. + (:issue:`364`) +- CodecContext.options no longer needs to be manually initialized. +- CodexContext.thread_type accepts its enums. + + +v0.4.1 +------ + +Minor: + +- Add `Frame.interlaced_frame` to indicate if the frame is interlaced. + (:issue:`327` by :gh-user:`MPGek`) +- Add FLTP support to ``Frame.to_nd_array()``. + (:issue:`288` by :gh-user:`rawler`) +- Expose ``CodecContext.extradata`` for codecs that have extra data, e.g. + Huffman tables. + (:issue:`287` by :gh-user:`adavoudi`) + +Patch: + +- Packets retain their refcount after muxing. + (:issue:`334`) +- `Codec` construction is more robust to find more codecs. + (:issue:`332` by :gh-user:`adavoudi`) +- Refined frame corruption detection. + (:issue:`291` by :gh-user:`Litterfeldt`) +- Unicode filenames are okay. + (:issue:`82`) + + +v0.4.0 +------ + +Major: + +- ``CodecContext`` has taken over encoding/decoding, and can work in isolation + of streams/containers. +- ``Stream.encode`` returns a list of packets, instead of a single packet. +- ``AudioFifo`` and ``AudioResampler`` will raise ``ValueError`` if input frames + inconsistant ``pts``. +- ``time_base`` use has been revisited across the codebase, and may not be converted + bettween ``Stream.time_base`` and ``CodecContext.time_base`` at the same times + in the transcoding pipeline. +- ``CodecContext.rate`` has been removed, but proxied to ``VideoCodecContext.framerate`` + and ``AudioCodecContext.sample_rate``. The definition is effectively inverted from + the old one (i.e. for 24fps it used to be ``1/24`` and is now ``24/1``). +- Fractions (e.g. ``time_base``, ``rate``) will be ``None`` if they are invalid. +- ``InputContainer.seek`` and ``Stream.seek`` will raise TypeError if given + a float, when previously they converted it from seconds. + +Minor: + +- Added ``Packet.is_keyframe`` and ``Packet.is_corrupt``. + (:issue:`226`) +- Many more ``time_base``, ``pts`` and other attributes are writeable. +- ``Option`` exposes much more of the API (but not get/set). + (:issue:`243`) +- Expose metadata encoding controls. + (:issue:`250`) +- Expose ``CodecContext.skip_frame``. + (:issue:`259`) + +Patch: + +- Build doesn't fail if you don't have git installed. + (:issue:`184`) +- Developer environment works better with Python3. + (:issue:`248`) +- Fix Container deallocation resulting in segfaults. + (:issue:`253`) + + +v0.3.3 +------ + +Patch: + +- Fix segfault due to buffer overflow in handling of stream options. + (:issue:`163` and :issue:`169`) +- Fix segfault due to seek not properly checking if codecs were open before + using avcodec_flush_buffers. + (:issue:`201`) + + +v0.3.2 +------ + +Minor: + +- Expose basics of avfilter via ``Filter``. +- Add ``Packet.time_base``. +- Add ``AudioFrame.to_nd_array`` to match same on ``VideoFrame``. +- Update Windows build process. + +Patch: + +- Further improvements to the logging system. + (:issue:`128`) + + +v0.3.1 +------ + +Minor: + +- ``av.logging.set_log_after_shutdown`` renamed to ``set_print_after_shutdown`` +- Repeating log messages will be skipped, much like ffmpeg's does by default + +Patch: + +- Fix memory leak in logging system when under heavy logging loads while + threading. + (:issue:`128` with help from :gh-user:`mkassner` and :gh-user:`ksze`) + + +v0.3.0 +------ + +Major: + +- Python IO can write +- Improve build system to use Python's C compiler for function detection; + build system is much more robust +- MSVC support. + (:issue:`115` by :gh-user:`vidartf`) +- Continuous integration on Windows via AppVeyor. (by :gh-user:`vidartf`) + +Minor: + +- Add ``Packet.decode_one()`` to skip packet flushing for codecs that would + otherwise error +- ``StreamContainer`` for easier selection of streams +- Add buffer protocol support to Packet + +Patch: + +- Fix bug when using Python IO on files larger than 2GB. + (:issue:`109` by :gh-user:`xxr3376`) +- Fix usage of changed Pillow API + +Known Issues: + +- VideoFrame is suspected to leak memory in narrow cases on Linux. + (:issue:`128`) + + +v0.2.4 +------ + +- fix library search path for current Libav/Ubuntu 14.04. + (:issue:`97`) +- explicitly include all sources to combat 0.2.3 release problem. + (:issue:`100`) + + +v0.2.3 +------ + +.. warning:: There was an issue with the PyPI distribution in which it required + Cython to be installed. + +Major: + +- Python IO. +- Agressively releases GIL +- Add experimental Windows build. + (:issue:`84`) + +Minor: + +- Several new Stream/Packet/Frame attributes + +Patch: + +- Fix segfault in audio handling. + (:issue:`86` and :issue:`93`) +- Fix use of PIL/Pillow API. + (:issue:`85`) +- Fix bad assumptions about plane counts. + (:issue:`76`) + + +v0.2.2 +------ + +- Cythonization in setup.py; mostly a development issue. +- Fix for av.InputContainer.size over 2**31. + + +v0.2.1 +------ + +- Python 3 compatibility! +- Build process fails if missing libraries. +- Fix linking of libavdevices. + + +v0.2.0 +------ + +.. warning:: This version has an issue linking in libavdevices, and very likely + will not work for you. + +It sure has been a long time since this was released, and there was a lot of +arbitrary changes that come with us wrapping an API as we are discovering it. +Changes include, but are not limited to: + +- Audio encoding. +- Exposing planes and buffers. +- Descriptors for channel layouts, video and audio formats, etc.. +- Seeking. +- Many many more properties on all of the objects. +- Device support (e.g. webcams). + + +v0.1.0 +------ + +- FIRST PUBLIC RELEASE! +- Container/video/audio formats. +- Audio layouts. +- Decoding video/audio/subtitles. +- Encoding video. +- Audio FIFOs and resampling. diff --git a/av/about.py b/av/about.py index 711c27229..13b4a72f4 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.1.0rc2" +__version__ = "12.1.0" diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index f41094eda..11ee1bf74 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -87,8 +87,8 @@ cdef class VideoReformatter: 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) + cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else 0) + cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else 0) return self._reformat( frame, diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..9ebbf3c5d --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,42 @@ + +SPHINXOPTS = +SPHINXBUILD = sphinx-build +BUILDDIR = _build +FFMPEGDIR = _ffmpeg + +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . + +.PHONY: clean html open upload default + +default: html + + +TAGFILE := _build/doxygen/tagfile.xml +$(TAGFILE) : + git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR) + ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) + + +TEMPLATES := $(wildcard api/*.py development/*.py) +RENDERED := $(TEMPLATES:%.py=_build/rst/%.rst) +_build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -or -name '*.pxd') + @ mkdir -p $(@D) + python $< > $@.tmp + mv $@.tmp $@ + + +clean: + rm -rf $(BUILDDIR) $(FFMPEGDIR) + +html: $(RENDERED) $(TAGFILE) + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + +test: + PYAV_SKIP_DOXYLINK=1 $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + +open: + open _build/html/index.html + +upload: + rsync -avxP --delete _build/html/ root@basswood-io.com:/var/www/pyav/docs/develop + diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 000000000..1069c8f1d --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,14 @@ + +.ffmpeg-quicklink { + float: right; + clear: right; + margin: 0; +} + +.ffmpeg-quicklink:before { + content: "["; +} + +.ffmpeg-quicklink:after { + content: "]"; +} diff --git a/docs/_static/examples/numpy/barcode.jpg b/docs/_static/examples/numpy/barcode.jpg new file mode 100644 index 000000000..4184c10f7 Binary files /dev/null and b/docs/_static/examples/numpy/barcode.jpg differ diff --git a/docs/_static/favicon.png b/docs/_static/favicon.png new file mode 100644 index 000000000..95ab33c6d Binary files /dev/null and b/docs/_static/favicon.png differ diff --git a/docs/_static/logo-250.png b/docs/_static/logo-250.png new file mode 100644 index 000000000..32cb6bfa1 Binary files /dev/null and b/docs/_static/logo-250.png differ diff --git a/docs/_themes/pyav/layout.html b/docs/_themes/pyav/layout.html new file mode 100644 index 000000000..a08771b5b --- /dev/null +++ b/docs/_themes/pyav/layout.html @@ -0,0 +1,12 @@ + +{%- extends "basic/layout.html" %} + +{% block extrahead %} + + +{% endblock %} + +{% block relbaritems %} +
  • +{% endblock %} + diff --git a/docs/_themes/pyav/theme.conf b/docs/_themes/pyav/theme.conf new file mode 100644 index 000000000..fa1636ba4 --- /dev/null +++ b/docs/_themes/pyav/theme.conf @@ -0,0 +1,2 @@ +[theme] +inherit = nature diff --git a/docs/api/_globals.rst b/docs/api/_globals.rst new file mode 100644 index 000000000..ef6ce329d --- /dev/null +++ b/docs/api/_globals.rst @@ -0,0 +1,5 @@ + +Globals +======= + +.. autofunction:: av.open diff --git a/docs/api/audio.rst b/docs/api/audio.rst new file mode 100644 index 000000000..bdff5fd4b --- /dev/null +++ b/docs/api/audio.rst @@ -0,0 +1,72 @@ + +Audio +===== + +Audio Streams +------------- + +.. automodule:: av.audio.stream + + .. autoclass:: AudioStream + :members: + +Audio Context +------------- + +.. automodule:: av.audio.codeccontext + + .. autoclass:: AudioCodecContext + :members: + :exclude-members: channel_layout, channels + +Audio Formats +------------- + +.. automodule:: av.audio.format + + .. autoclass:: AudioFormat + :members: + +Audio Layouts +------------- + +.. automodule:: av.audio.layout + + .. autoclass:: AudioLayout + :members: + + .. autoclass:: AudioChannel + :members: + +Audio Frames +------------ + +.. automodule:: av.audio.frame + + .. autoclass:: AudioFrame + :members: + :exclude-members: to_nd_array + +Audio FIFOs +----------- + +.. automodule:: av.audio.fifo + + .. autoclass:: AudioFifo + :members: + :exclude-members: write, read, read_many + + .. automethod:: write + .. automethod:: read + .. automethod:: read_many + +Audio Resamplers +---------------- + +.. automodule:: av.audio.resampler + + .. autoclass:: AudioResampler + :members: + :exclude-members: resample + + .. automethod:: resample diff --git a/docs/api/bitstream.rst b/docs/api/bitstream.rst new file mode 100644 index 000000000..4dab1bde5 --- /dev/null +++ b/docs/api/bitstream.rst @@ -0,0 +1,9 @@ + +Bitstream Filters +================= + +.. automodule:: av.bitstream + + .. autoclass:: BitStreamFilterContext + :members: + diff --git a/docs/api/buffer.rst b/docs/api/buffer.rst new file mode 100644 index 000000000..eabe5ba11 --- /dev/null +++ b/docs/api/buffer.rst @@ -0,0 +1,8 @@ + +Buffers +======= + +.. automodule:: av.buffer + + .. autoclass:: Buffer + :members: diff --git a/docs/api/codec.rst b/docs/api/codec.rst new file mode 100644 index 000000000..ebc147c30 --- /dev/null +++ b/docs/api/codec.rst @@ -0,0 +1,135 @@ + +Codecs +====== + +Descriptors +----------- + +.. currentmodule:: av.codec +.. automodule:: av.codec + +.. autoclass:: Codec + +.. automethod:: Codec.create + +.. autoattribute:: Codec.is_decoder +.. autoattribute:: Codec.is_encoder + +.. autoattribute:: Codec.descriptor +.. autoattribute:: Codec.name +.. autoattribute:: Codec.long_name +.. autoattribute:: Codec.type +.. autoattribute:: Codec.id + +.. autoattribute:: Codec.frame_rates +.. autoattribute:: Codec.audio_rates +.. autoattribute:: Codec.video_formats +.. autoattribute:: Codec.audio_formats + + +Flags +~~~~~ + +.. autoattribute:: Codec.properties + +.. autoclass:: Properties + + Wraps :ffmpeg:`AVCodecDescriptor.props` (``AV_CODEC_PROP_*``). + + .. enumtable:: av.codec.codec.Properties + :class: av.codec.codec.Codec + +.. autoattribute:: Codec.capabilities + +.. autoclass:: Capabilities + + Wraps :ffmpeg:`AVCodec.capabilities` (``AV_CODEC_CAP_*``). + + Note that ``ffmpeg -codecs`` prefers the properties versions of + ``INTRA_ONLY`` and ``LOSSLESS``. + + .. enumtable:: av.codec.codec.Capabilities + :class: av.codec.codec.Codec + + +Contexts +-------- + +.. currentmodule:: av.codec.context +.. automodule:: av.codec.context + +.. autoclass:: CodecContext + +.. autoattribute:: CodecContext.codec +.. autoattribute:: CodecContext.options + +.. automethod:: CodecContext.create +.. automethod:: CodecContext.open +.. automethod:: CodecContext.close + +Attributes +~~~~~~~~~~ + +.. autoattribute:: CodecContext.is_open +.. autoattribute:: CodecContext.is_encoder +.. autoattribute:: CodecContext.is_decoder + +.. autoattribute:: CodecContext.name +.. autoattribute:: CodecContext.type +.. autoattribute:: CodecContext.profile + +.. autoattribute:: CodecContext.time_base +.. autoattribute:: CodecContext.ticks_per_frame + +.. autoattribute:: CodecContext.bit_rate +.. autoattribute:: CodecContext.bit_rate_tolerance +.. autoattribute:: CodecContext.max_bit_rate + +.. autoattribute:: CodecContext.thread_count +.. autoattribute:: CodecContext.thread_type +.. autoattribute:: CodecContext.skip_frame + +.. autoattribute:: CodecContext.extradata +.. autoattribute:: CodecContext.extradata_size + +Transcoding +~~~~~~~~~~~ +.. automethod:: CodecContext.parse +.. automethod:: CodecContext.encode +.. automethod:: CodecContext.decode + + +Flags +~~~~~ + +.. autoattribute:: CodecContext.flags + +.. autoclass:: av.codec.context.Flags + + .. enumtable:: av.codec.context:Flags + :class: av.codec.context:CodecContext + +.. autoattribute:: CodecContext.flags2 + +.. autoclass:: av.codec.context.Flags2 + + .. enumtable:: av.codec.context:Flags2 + :class: av.codec.context:CodecContext + + +Enums +~~~~~ + +.. autoclass:: av.codec.context.ThreadType + + Which multithreading methods to use. + Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread, + so clients which cannot provide future frames should not use it. + + .. enumtable:: av.codec.context.ThreadType + +.. autoclass:: av.codec.context.SkipType + + .. enumtable:: av.codec.context.SkipType + + diff --git a/docs/api/container.rst b/docs/api/container.rst new file mode 100644 index 000000000..f4c9732c9 --- /dev/null +++ b/docs/api/container.rst @@ -0,0 +1,79 @@ + +Containers +========== + + +Generic +------- + +.. currentmodule:: av.container + +.. automodule:: av.container + +.. autoclass:: Container + + .. attribute:: options + .. attribute:: container_options + .. attribute:: stream_options + .. attribute:: metadata_encoding + .. attribute:: metadata_errors + .. attribute:: open_timeout + .. attribute:: read_timeout + + +Flags +~~~~~ + +.. attribute:: av.container.Container.flags + +.. class:: av.container.Flags + + Wraps :ffmpeg:`AVFormatContext.flags`. + + .. enumtable:: av.container.core:Flags + :class: av.container.core:Container + + +Input Containers +---------------- + +.. autoclass:: InputContainer + :members: + + +Output Containers +----------------- + +.. autoclass:: OutputContainer + :members: + + +Formats +------- + +.. currentmodule:: av.format + +.. automodule:: av.format + +.. autoclass:: ContainerFormat + +.. autoattribute:: ContainerFormat.name +.. autoattribute:: ContainerFormat.long_name + +.. autoattribute:: ContainerFormat.options +.. autoattribute:: ContainerFormat.input +.. autoattribute:: ContainerFormat.output +.. autoattribute:: ContainerFormat.is_input +.. autoattribute:: ContainerFormat.is_output +.. autoattribute:: ContainerFormat.extensions + +Flags +~~~~~ + +.. autoattribute:: ContainerFormat.flags + +.. autoclass:: av.format.Flags + + .. enumtable:: av.format.Flags + :class: av.format.ContainerFormat + diff --git a/docs/api/enum.rst b/docs/api/enum.rst new file mode 100644 index 000000000..5fdcec4f7 --- /dev/null +++ b/docs/api/enum.rst @@ -0,0 +1,24 @@ + +Enumerations and Flags +====================== + +.. currentmodule:: av.enum + +.. automodule:: av.enum + + +.. _enums: + +Enumerations +------------ + +.. autoclass:: EnumItem + + +.. _flags: + +Flags +----- + +.. autoclass:: EnumFlag + diff --git a/docs/api/error.rst b/docs/api/error.rst new file mode 100644 index 000000000..3f82f4ec2 --- /dev/null +++ b/docs/api/error.rst @@ -0,0 +1,85 @@ +Errors +====== + +.. currentmodule:: av.error + +.. _error_behaviour: + +General Behaviour +----------------- + +When PyAV encounters an FFmpeg error, it raises an appropriate exception. + +FFmpeg has a couple dozen of its own error types which we represent via +:ref:`error_classes` and at a lower level via :ref:`error_types`. + +FFmpeg will also return more typical errors such as ``ENOENT`` or ``EAGAIN``, +which we do our best to translate to extensions of the builtin exceptions +as defined by +`PEP 3151 `_ +(and fall back onto ``OSError`` if using Python < 3.3). + + +.. _error_types: + +Error Type Enumerations +----------------------- + +We provide :class:`av.error.ErrorType` as an enumeration of the various FFmpeg errors. +To mimick the stdlib ``errno`` module, all enumeration values are available in +the ``av.error`` module, e.g.:: + + try: + do_something() + except OSError as e: + if e.errno != av.error.FILTER_NOT_FOUND: + raise + handle_error() + + +.. autoclass:: av.error.ErrorType + + +.. _error_classes: + +Error Exception Classes +----------------------- + +PyAV raises the typical builtin exceptions within its own codebase, but things +get a little more complex when it comes to translating FFmpeg errors. + +There are two competing ideas that have influenced the final design: + +1. We want every exception that originates within FFmpeg to inherit from a common + :class:`.FFmpegError` exception; + +2. We want to use the builtin exceptions whenever possible. + +As such, PyAV effectivly shadows as much of the builtin exception heirarchy as +it requires, extending from both the builtins and from :class:`FFmpegError`. + +Therefore, an argument error within FFmpeg will raise a ``av.error.ValueError``, which +can be caught via either :class:`FFmpegError` or ``ValueError``. All of these +exceptions expose the typical ``errno`` and ``strerror`` attributes (even +``ValueError`` which doesn't typically), as well as some PyAV extensions such +as :attr:`FFmpegError.log`. + +All of these exceptions are available on the top-level ``av`` package, e.g.:: + + try: + do_something() + except av.FilterNotFoundError: + handle_error() + + +.. autoclass:: av.FFmpegError + + +Mapping Codes and Classes +------------------------- + +Here is how the classes line up with the error codes/enumerations: + +.. include:: ../_build/rst/api/error_table.rst + + diff --git a/docs/api/error_table.py b/docs/api/error_table.py new file mode 100644 index 000000000..e67b9f40b --- /dev/null +++ b/docs/api/error_table.py @@ -0,0 +1,37 @@ + +import av + + +rows = [( + #'Tag (Code)', + 'Exception Class', + 'Code/Enum Name', + 'FFmpeg Error Message', +)] + +for code, cls in av.error.classes.items(): + + enum = av.error.ErrorType.get(code) + + if not enum: + continue + + if enum.tag == b'PyAV': + continue + + rows.append(( + #'{} ({})'.format(enum.tag, code), + '``av.{}``'.format(cls.__name__), + '``av.error.{}``'.format(enum.name), + enum.strerror, + )) + +lens = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] + +header = tuple('=' * x for x in lens) +rows.insert(0, header) +rows.insert(2, header) +rows.append(header) + +for row in rows: + print(' '.join('{:{}s}'.format(cell, len_) for cell, len_ in zip(row, lens))) diff --git a/docs/api/filter.rst b/docs/api/filter.rst new file mode 100644 index 000000000..d126fda67 --- /dev/null +++ b/docs/api/filter.rst @@ -0,0 +1,34 @@ +Filters +======= + +.. automodule:: av.filter.filter + + .. autoclass:: Filter + :members: + + +.. automodule:: av.filter.graph + + .. autoclass:: Graph + :members: + + +.. automodule:: av.filter.context + + .. autoclass:: FilterContext + :members: + + +.. automodule:: av.filter.link + + .. autoclass:: FilterLink + :members: + + +.. automodule:: av.filter.pad + + .. autoclass:: FilterPad + :members: + + .. autoclass:: FilterContextPad + :members: diff --git a/docs/api/frame.rst b/docs/api/frame.rst new file mode 100644 index 000000000..f0e3a7554 --- /dev/null +++ b/docs/api/frame.rst @@ -0,0 +1,8 @@ + +Frames +====== + +.. automodule:: av.frame + + .. autoclass:: Frame + :members: diff --git a/docs/api/packet.rst b/docs/api/packet.rst new file mode 100644 index 000000000..315d08345 --- /dev/null +++ b/docs/api/packet.rst @@ -0,0 +1,8 @@ + +Packets +======= + +.. automodule:: av.packet + + .. autoclass:: Packet + :members: diff --git a/docs/api/plane.rst b/docs/api/plane.rst new file mode 100644 index 000000000..7310f963f --- /dev/null +++ b/docs/api/plane.rst @@ -0,0 +1,8 @@ + +Planes +====== + +.. automodule:: av.plane + + .. autoclass:: Plane + :members: diff --git a/docs/api/sidedata.rst b/docs/api/sidedata.rst new file mode 100644 index 000000000..48163b09f --- /dev/null +++ b/docs/api/sidedata.rst @@ -0,0 +1,20 @@ + +Side Data +========= + +.. automodule:: av.sidedata.sidedata + + .. autoclass:: SideData + :members: + +.. autoclass:: av.sidedata.sidedata.Type +.. enumtable:: av.sidedata.sidedata.Type + + +Motion Vectors +-------------- + +.. automodule:: av.sidedata.motionvectors + + .. autoclass:: MotionVectors + :members: diff --git a/docs/api/stream.rst b/docs/api/stream.rst new file mode 100644 index 000000000..a6bc1fc8b --- /dev/null +++ b/docs/api/stream.rst @@ -0,0 +1,90 @@ + +Streams +======= + + +Stream collections +------------------ + +.. currentmodule:: av.container.streams + +.. autoclass:: StreamContainer + + +Dynamic Slicing +~~~~~~~~~~~~~~~ + +.. automethod:: StreamContainer.get + + +Typed Collections +~~~~~~~~~~~~~~~~~ + +These attributes are preferred for readability if you don't need the +dynamic capabilities of :meth:`.get`: + +.. attribute:: StreamContainer.video + + A tuple of :class:`VideoStream`. + +.. attribute:: StreamContainer.audio + + A tuple of :class:`AudioStream`. + +.. attribute:: StreamContainer.subtitles + + A tuple of :class:`SubtitleStream`. + +.. attribute:: StreamContainer.data + + A tuple of :class:`DataStream`. + +.. attribute:: StreamContainer.other + + A tuple of :class:`Stream` + + +Streams +------- + +.. currentmodule:: av.stream + +.. autoclass:: Stream + + +Basics +~~~~~~ + + +.. autoattribute:: Stream.type + +.. autoattribute:: Stream.codec_context + +.. autoattribute:: Stream.id + +.. autoattribute:: Stream.index + + +Timing +~~~~~~ + +.. seealso:: :ref:`time` for a discussion of time in general. + +.. autoattribute:: Stream.time_base + +.. autoattribute:: Stream.start_time + +.. autoattribute:: Stream.duration + +.. autoattribute:: Stream.frames + + +Others +~~~~~~ + +.. autoattribute:: Stream.profile + +.. autoattribute:: Stream.language + + + diff --git a/docs/api/subtitles.rst b/docs/api/subtitles.rst new file mode 100644 index 000000000..19d75621c --- /dev/null +++ b/docs/api/subtitles.rst @@ -0,0 +1,28 @@ + +Subtitles +=========== + +.. automodule:: av.subtitles.stream + + .. autoclass:: SubtitleStream + :members: + +.. automodule:: av.subtitles.subtitle + + .. autoclass:: SubtitleSet + :members: + + .. autoclass:: Subtitle + :members: + + .. autoclass:: BitmapSubtitle + :members: + + .. autoclass:: BitmapSubtitlePlane + :members: + + .. autoclass:: TextSubtitle + :members: + + .. autoclass:: AssSubtitle + :members: diff --git a/docs/api/time.rst b/docs/api/time.rst new file mode 100644 index 000000000..fd65de1a2 --- /dev/null +++ b/docs/api/time.rst @@ -0,0 +1,92 @@ + +.. _time: + +Time +==== + +Overview +-------- + +Time is expressed as integer multiples of arbitrary units of time called a ``time_base``. There are different contexts that have different time bases: :class:`.Stream` has :attr:`.Stream.time_base`, :class:`.CodecContext` has :attr:`.CodecContext.time_base`, and :class:`.Container` has :data:`av.TIME_BASE`. + +.. testsetup:: + + import av + path = av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4') + + def get_nth_packet_and_frame(fh, skip): + for p in fh.demux(): + for f in p.decode(): + if not skip: + return p, f + skip -= 1 + +.. doctest:: + + >>> fh = av.open(path) + >>> video = fh.streams.video[0] + + >>> video.time_base + Fraction(1, 25) + +Attributes that represent time on those objects will be in that object's ``time_base``: + +.. doctest:: + + >>> video.duration + 168 + >>> float(video.duration * video.time_base) + 6.72 + +: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.. + +.. doctest:: + + >>> p, f = get_nth_packet_and_frame(fh, skip=1) + + >>> p.time_base + Fraction(1, 25) + >>> p.dts + 1 + + >>> f.time_base + Fraction(1, 25) + >>> f.pts + 1 + + +For convenince, :attr:`.Frame.time` is a ``float`` in seconds: + +.. doctest:: + + >>> f.time + 0.04 + + +FFMpeg Internals +---------------- + +.. note:: Time in FFmpeg is not 100% clear to us (see :ref:`authority_of_docs`). At times the FFmpeg documentation and canonical seeming posts in the forums appear contradictory. We've experiemented with it, and what follows is the picture that we are operating under. + +Both :ffmpeg:`AVStream` and :ffmpeg:`AVCodecContext` have a ``time_base`` member. However, they are used for different purposes, and (this author finds) it is too easy to abstract the concept too far. + +When there is no ``time_base`` (such as on :ffmpeg:`AVFormatContext`), there is an implicit ``time_base`` of ``1/AV_TIME_BASE``. + +Encoding +........ + + +For encoding, you (the PyAV developer / FFmpeg "user") must set :ffmpeg:`AVCodecContext.time_base`, ideally to the inverse of the frame rate (or so the library docs say to do if your frame rate is fixed; we're not sure what to do if it is not fixed), and you may set :ffmpeg:`AVStream.time_base` as a hint to the muxer. After you open all the codecs and call :ffmpeg:`avformat_write_header`, the stream time base may change, and you must respect it. We don't know if the codec time base may change, so we will make the safer assumption that it may and respect it as well. + +You then prepare :ffmpeg:`AVFrame.pts` in :ffmpeg:`AVCodecContext.time_base`. The encoded :ffmpeg:`AVPacket.pts` is simply copied from the frame by the library, and so is still in the codec's time base. You must rescale it to :ffmpeg:`AVStream.time_base` before muxing (as all stream operations assume the packet time is in stream time base). + +For fixed-fps content your frames' ``pts`` would be the frame or sample index (for video and audio, respectively). PyAV should attempt to do this. + + +Decoding +........ + +Everything is in :ffmpeg:`AVStream.time_base` because we don't have to rebase it into codec time base (as it generally seems to be the case that :ffmpeg:`AVCodecContext` doesn't really care about your timing; I wish there was a way to assert this without reading every codec). + diff --git a/docs/api/utils.rst b/docs/api/utils.rst new file mode 100644 index 000000000..820f30f0a --- /dev/null +++ b/docs/api/utils.rst @@ -0,0 +1,18 @@ + + +Utilities +========= + +Logging +------- + +.. automodule:: av.logging + :members: + +Other +----- + +.. automodule:: av.utils + :members: + + .. autoclass:: AVError diff --git a/docs/api/video.rst b/docs/api/video.rst new file mode 100644 index 000000000..5e47b1db8 --- /dev/null +++ b/docs/api/video.rst @@ -0,0 +1,119 @@ +Video +===== + +Video Streams +------------- + +.. automodule:: av.video.stream + + .. autoclass:: VideoStream + :members: + +Video Codecs +------------- + +.. automodule:: av.video.codeccontext + + .. autoclass:: VideoCodecContext + :members: + +Video Formats +------------- + +.. automodule:: av.video.format + + .. autoclass:: VideoFormat + :members: + + .. autoclass:: VideoFormatComponent + :members: + +Video Frames +------------ + +.. automodule:: av.video.frame + +.. autoclass:: VideoFrame + + A single video frame. + + :param int width: The width of the frame. + :param int height: The height of the frame. + :param format: The format of the frame. + :type format: :class:`VideoFormat` or ``str``. + + >>> frame = VideoFrame(1920, 1080, 'rgb24') + +Structural +~~~~~~~~~~ + +.. autoattribute:: VideoFrame.width +.. autoattribute:: VideoFrame.height +.. attribute:: VideoFrame.format + + The :class:`.VideoFormat` of the frame. + +.. autoattribute:: VideoFrame.planes + +Types +~~~~~ + +.. autoattribute:: VideoFrame.key_frame +.. autoattribute:: VideoFrame.interlaced_frame +.. autoattribute:: VideoFrame.pict_type + +.. autoclass:: av.video.frame.PictureType + + Wraps ``AVPictureType`` (``AV_PICTURE_TYPE_*``). + + .. enumtable:: av.video.frame.PictureType + + +Conversions +~~~~~~~~~~~ + +.. automethod:: VideoFrame.reformat + +.. automethod:: VideoFrame.to_rgb +.. automethod:: VideoFrame.to_image +.. automethod:: VideoFrame.to_ndarray + +.. automethod:: VideoFrame.from_image +.. automethod:: VideoFrame.from_ndarray + + + +Video Planes +------------- + +.. automodule:: av.video.plane + + .. autoclass:: VideoPlane + :members: + + +Video Reformatters +------------------ + +.. automodule:: av.video.reformatter + + .. autoclass:: VideoReformatter + + .. automethod:: reformat + +Enums +~~~~~ + +.. autoclass:: av.video.reformatter.Interpolation + + Wraps the ``SWS_*`` flags. + + .. enumtable:: av.video.reformatter.Interpolation + +.. autoclass:: av.video.reformatter.Colorspace + + Wraps the ``SWS_CS_*`` flags. There is a bit of overlap in + these names which comes from FFmpeg and backards compatibility. + + .. enumtable:: av.video.reformatter.Colorspace + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..a893a631c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,365 @@ +import logging +import os +import re +import sys +import xml.etree.ElementTree as etree + +from docutils import nodes +from sphinx import addnodes +from sphinx.util.docutils import SphinxDirective +import sphinx + + +logging.basicConfig() + +# 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("..")) + +# -- General configuration ----------------------------------------------------- + +# 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", +] + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# General information about the project. +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: + exec(fp.read(), about) + +# The full version, including alpha/beta/rc tags. +release = about["__version__"] + +# The short X.Y version. +version = release.split("-")[0] + +exclude_patterns = ["_build"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# -- Options for HTML output --------------------------------------------------- + +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" + +# 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" + +# 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"] + + +doctest_global_setup = """ + +import errno +import os + +import av +from av.datasets import fate, fate as fate_suite, curated + +from tests import common +from tests.common import sandboxed as _sandboxed + +def sandboxed(*args, **kwargs): + kwargs['timed'] = True + return _sandboxed('docs', *args, **kwargs) + +_cwd = os.getcwd() +here = sandboxed('__cwd__') +try: + os.makedirs(here) +except OSError as e: + if e.errno != errno.EEXIST: + raise +os.chdir(here) + +video_path = curated('pexels/time-lapse-video-of-night-sky-857195.mp4') + +""" + +doctest_global_cleanup = """ + +os.chdir(_cwd) + +""" + + +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", "@"), +} + +intersphinx_mapping = { + "https://docs.python.org/3": None, +} + +autodoc_member_order = "bysource" +autodoc_default_options = { + "undoc-members": True, + "show-inheritance": True, +} + + +todo_include_todos = True + + +class PyInclude(SphinxDirective): + has_content = True + + def run(self): + source = "\n".join(self.content) + output = [] + + def write(*content, sep=" ", end="\n"): + output.append(sep.join(map(str, content)) + end) + + namespace = dict(write=write) + exec(compile(source, "", "exec"), namespace, namespace) + + output = "".join(output).splitlines() + self.state_machine.insert_input(output, "blah") + + return [] # [nodes.literal('hello', repr(content))] + + +def load_entrypoint(name): + parts = name.split(":") + if len(parts) == 1: + parts = name.rsplit(".", 1) + mod_name, attrs = parts + + attrs = attrs.split(".") + try: + obj = __import__(mod_name, fromlist=["."]) + except ImportError as e: + print("Error while importing.", (name, mod_name, attrs, e)) + raise + + for attr in attrs: + obj = getattr(obj, attr) + + return obj + + +class EnumTable(SphinxDirective): + required_arguments = 1 + option_spec = { + "class": lambda x: x, + } + + def run(self): + 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: + for name, value in vars(cls).items(): + if isinstance(value, property): + try: + item = value._enum_item + except AttributeError: + pass + else: + if isinstance(item, enum): + properties[item] = name + + colwidths = [15, 15, 5, 65] if cls else [15, 5, 75] + ncols = len(colwidths) + + table = nodes.table() + + tgroup = nodes.tgroup(cols=ncols) + table += tgroup + + for width in colwidths: + tgroup += nodes.colspec(colwidth=width) + + thead = nodes.thead() + tgroup += thead + + tbody = nodes.tbody() + tgroup += tbody + + def makerow(*texts): + row = nodes.row() + for text in texts: + if text is None: + continue + row += nodes.entry("", nodes.paragraph("", str(text))) + return row + + thead += makerow( + 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()) + + try: + attr = properties[item] + except KeyError: + if cls: + continue + attr = None + + 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") +) +if not os.path.exists(ffmpeg_tagfile): + print("ERROR: Missing FFmpeg tagfile.") + exit(1) +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) + root = doc.getroot() + + parent_map = {} # ElementTree doesn't five us access to parents. + urls = {} + + 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"): + continue + + name = node.find("name").text + + if kind not in ("function",): + parent = parent_map.get(node) + parent_name = parent.find("name") if parent else None + if parent_name is not None: + name = f"{parent_name.text}.{name}" + + filenode = node.find("filename") + if filenode is not None: + url = filenode.text + else: + url = "{}#{}".format( + node.find("anchorfile").text, + node.find("anchor").text, + ) + + urls.setdefault(kind, {})[name] = url + + def get_url(name): + # These are all the kinds that seem to exist. + for kind in ( + "function", + "struct", + "variable", # These are struct members. + # 'class', + # 'define', + # 'enumeration', + # 'enumvalue', + # 'file', + # 'group', + # 'page', + # 'typedef', + # 'union', + ): + try: + return urls[kind][name] + except KeyError: + pass + + def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content=[]): + m = re.match(r"^(.+?)(?:<(.+?)>)?$", text) + title, name = m.groups() + name = name or title + + url = get_url(name) + if not url: + 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) + + return [node], [] + + return _doxylink_handler + + +def setup(app): + app.add_css_file("custom.css") + + app.add_directive("flagtable", EnumTable) + app.add_directive("enumtable", EnumTable) + app.add_directive("pyinclude", PyInclude) + + 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/cookbook/basics.rst b/docs/cookbook/basics.rst new file mode 100644 index 000000000..2896519de --- /dev/null +++ b/docs/cookbook/basics.rst @@ -0,0 +1,42 @@ +Basics +====== + +Here are some common things to do without digging too deep into the mechanics. + + +Saving Keyframes +---------------- + +If you just want to look at keyframes, you can set :attr:`.CodecContext.skip_frame` to speed up the process: + +.. literalinclude:: ../../examples/basics/save_keyframes.py + + +Remuxing +-------- + +Remuxing is copying audio/video data from one container to the other without transcoding it. By doing so, the data does not suffer any generational loss, and is the full quality that it was in the source container. + +.. literalinclude:: ../../examples/basics/remux.py + + +Parsing +------- + +Sometimes we have a raw stream of data, and we need to split it into packets before working with it. We can use :meth:`.CodecContext.parse` to do this. + +.. literalinclude:: ../../examples/basics/parse.py + + +Threading +--------- + +By default, codec contexts will decode with :data:`~av.codec.context.ThreadType.SLICE` threading. This allows multiple threads to cooperate to decode any given frame. + +This is faster than no threading, but is not as fast as we can go. + +Also enabling :data:`~av.codec.context.ThreadType.FRAME` (or :data:`~av.codec.context.ThreadType.AUTO`) threading allows multiple threads to decode independent frames. This is not enabled by default because it does change the API a bit: you will get a much larger "delay" between starting the decode of a packet and getting it's results. Take a look at the output of this sample to see what we mean: + +.. literalinclude:: ../../examples/basics/thread_type.py + +On the author's machine, the second pass decodes ~5 times faster. diff --git a/docs/cookbook/numpy.rst b/docs/cookbook/numpy.rst new file mode 100644 index 000000000..d4887945c --- /dev/null +++ b/docs/cookbook/numpy.rst @@ -0,0 +1,24 @@ +Numpy +===== + + +Video Barcode +------------- + +A video barcode shows the change in colour and tone over time. Time is represented on the horizontal axis, while the vertical remains the vertical direction in the image. + +See http://moviebarcode.tumblr.com/ for examples from Hollywood movies, and here is an example from a sunset timelapse: + +.. image:: ../_static/examples/numpy/barcode.jpg + +The code that created this: + +.. literalinclude:: ../../examples/numpy/barcode.py + + +Generating Video +---------------- + +.. literalinclude:: ../../examples/numpy/generate_video.py + + diff --git a/docs/development/changelog.rst b/docs/development/changelog.rst new file mode 100644 index 000000000..339b4474e --- /dev/null +++ b/docs/development/changelog.rst @@ -0,0 +1,6 @@ + +.. It is all in the other file (that we want at the top-level of the repo). + +.. _changelog: + +.. include:: ../../CHANGELOG.rst diff --git a/docs/development/contributors.rst b/docs/development/contributors.rst new file mode 100644 index 000000000..a17b40630 --- /dev/null +++ b/docs/development/contributors.rst @@ -0,0 +1,3 @@ + + +.. include:: ../../AUTHORS.rst diff --git a/docs/development/includes.py b/docs/development/includes.py new file mode 100644 index 000000000..8f350a81e --- /dev/null +++ b/docs/development/includes.py @@ -0,0 +1,365 @@ +import json +import os +import re +import sys + +import xml.etree.ElementTree as etree + +from Cython.Compiler.Main import CompilationOptions, Context +from Cython.Compiler.TreeFragment import parse_from_strings +from Cython.Compiler.Visitor import TreeVisitor +from Cython.Compiler import Nodes + +os.chdir(os.path.abspath(os.path.join(__file__, '..', '..', '..'))) + + +class Visitor(TreeVisitor): + + def __init__(self, state=None): + super(Visitor, self).__init__() + self.state = dict(state or {}) + self.events = [] + + def record_event(self, node, **kw): + state = self.state.copy() + state.update(**kw) + state['node'] = node + state['pos'] = node.pos + state['end_pos'] = node.end_pos() + self.events.append(state) + + def visit_Node(self, node): + self.visitchildren(node) + + def visit_ModuleNode(self, node): + self.state['module'] = node.full_module_name + self.visitchildren(node) + self.state.pop('module') + + def visit_CDefExternNode(self, node): + self.state['extern_from'] = node.include_file + self.visitchildren(node) + self.state.pop('extern_from') + + def visit_CStructOrUnionDefNode(self, node): + self.record_event(node, type='struct', name=node.name) + self.state['struct'] = node.name + self.visitchildren(node) + self.state.pop('struct') + + def visit_CFuncDeclaratorNode(self, node): + if isinstance(node.base, Nodes.CNameDeclaratorNode): + self.record_event(node, type='function', name=node.base.name) + else: + self.visitchildren(node) + + def visit_CVarDefNode(self, node): + + if isinstance(node.declarators[0], Nodes.CNameDeclaratorNode): + + # Grab the type name. + # TODO: Do a better job. + type_ = node.base_type + if hasattr(type_, 'name'): + type_name = type_.name + elif hasattr(type_, 'base_type'): + type_name = type_.base_type.name + else: + type_name = str(type_) + + self.record_event(node, type='variable', name=node.declarators[0].name, + vartype=type_name) + + else: + self.visitchildren(node) + + def visit_CClassDefNode(self, node): + self.state['class'] = node.class_name + self.visitchildren(node) + self.state.pop('class') + + def visit_PropertyNode(self, node): + self.state['property'] = node.name + self.visitchildren(node) + self.state.pop('property') + + def visit_DefNode(self, node): + self.state['function'] = node.name + self.visitchildren(node) + self.state.pop('function') + + def visit_AttributeNode(self, node): + if getattr(node.obj, 'name', None) == 'lib': + self.record_event(node, type='use', name=node.attribute) + else: + self.visitchildren(node) + + +def extract(path, **kwargs): + + name = os.path.splitext(os.path.relpath(path))[0].replace('/', '.') + + options = CompilationOptions() + options.include_path.append('include') + options.language_level = 2 + options.compiler_directives = dict( + c_string_type='str', + c_string_encoding='ascii', + ) + + context = Context( + options.include_path, + options.compiler_directives, + options.cplus, + options.language_level, + options=options, + ) + + tree = parse_from_strings( + name, open(path).read(), context, + level='module_pxd' if path.endswith('.pxd') else None, + **kwargs) + + extractor = Visitor({'file': path}) + extractor.visit(tree) + return extractor.events + + +def iter_cython(path): + '''Yield all ``.pyx`` and ``.pxd`` files in the given root.''' + for dir_path, dir_names, file_names in os.walk(path): + for file_name in file_names: + if file_name.startswith('.'): + continue + if os.path.splitext(file_name)[1] not in ('.pyx', '.pxd'): + continue + yield os.path.join(dir_path, file_name) + + +doxygen = {} +doxygen_base = 'https://ffmpeg.org/doxygen/trunk' +tagfile_path = 'docs/_build/doxygen/tagfile.xml' + +tagfile_json = tagfile_path + '.json' +if os.path.exists(tagfile_json): + print('Loading pre-parsed Doxygen tagfile:', tagfile_json, file=sys.stderr) + doxygen = json.load(open(tagfile_json)) + + +if not doxygen: + + print('Parsing Doxygen tagfile:', tagfile_path, file=sys.stderr) + if not os.path.exists(tagfile_path): + print(' MISSING!', file=sys.stderr) + else: + + root = etree.parse(tagfile_path) + + def inspect_member(node, name_prefix=''): + name = name_prefix + node.find('name').text + anchorfile = node.find('anchorfile').text + anchor = node.find('anchor').text + + url = '%s/%s#%s' % (doxygen_base, anchorfile, anchor) + + doxygen[name] = {'url': url} + + if node.attrib['kind'] == 'function': + ret_type = node.find('type').text + arglist = node.find('arglist').text + sig = '%s %s%s' % (ret_type, name, arglist) + doxygen[name]['sig'] = sig + + for struct in root.iter('compound'): + if struct.attrib['kind'] != 'struct': + continue + name_prefix = struct.find('name').text + '.' + for node in struct.iter('member'): + inspect_member(node, name_prefix) + + for node in root.iter('member'): + inspect_member(node) + + + json.dump(doxygen, open(tagfile_json, 'w'), sort_keys=True, indent=4) + + +print('Parsing Cython source for references...', file=sys.stderr) +lib_references = {} +for path in iter_cython('av'): + try: + events = extract(path) + except Exception as e: + print(" %s in %s" % (e.__class__.__name__, path), file=sys.stderr) + print(" %s" % e, file=sys.stderr) + continue + for event in events: + if event['type'] == 'use': + lib_references.setdefault(event['name'], []).append(event) + + + + + + + +defs_by_extern = {} +for path in iter_cython('include'): + + # This one has "include" directives, which is not supported when + # parsing from a string. + if path == 'include/libav.pxd': + continue + + # Extract all #: comments from the source files. + comments_by_line = {} + for i, line in enumerate(open(path)): + m = re.match(r'^\s*#: ?', line) + if m: + comment = line[m.end():].rstrip() + comments_by_line[i + 1] = line[m.end():] + + # Extract Cython definitions from the source files. + for event in extract(path): + + extern = event.get('extern_from') or path.replace('include/', '') + defs_by_extern.setdefault(extern, []).append(event) + + # Collect comments above and below + comments = event['_comments'] = [] + line = event['pos'][1] - 1 + while line in comments_by_line: + comments.insert(0, comments_by_line.pop(line)) + line -= 1 + line = event['end_pos'][1] + 1 + while line in comments_by_line: + comments.append(comments_by_line.pop(line)) + line += 1 + + # Figure out the Sphinx headline. + if event['type'] == 'function': + event['_sort_key'] = 2 + sig = doxygen.get(event['name'], {}).get('sig') + if sig: + sig = re.sub(r'\).+', ')', sig) # strip trailer + event['_headline'] = '.. c:function:: %s' % sig + else: + event['_headline'] = '.. c:function:: %s()' % event['name'] + + elif event['type'] == 'variable': + struct = event.get('struct') + if struct: + event['_headline'] = '.. c:member:: %s %s' % (event['vartype'], event['name']) + event['_sort_key'] = 1.1 + else: + event['_headline'] = '.. c:var:: %s' % event['name'] + event['_sort_key'] = 3 + + elif event['type'] == 'struct': + event['_headline'] = '.. c:type:: struct %s' % event['name'] + event['_sort_key'] = 1 + event['_doxygen_url'] = '%s/struct%s.html' % (doxygen_base, event['name']) + + else: + print('Unknown event type %s' % event['type'], file=sys.stderr) + + name = event['name'] + if event.get('struct'): + name = '%s.%s' % (event['struct'], name) + + # Doxygen URLs + event.setdefault('_doxygen_url', doxygen.get(name, {}).get('url')) + + # Find use references. + ref_events = lib_references.get(name, []) + if ref_events: + + ref_pairs = [] + for ref in sorted(ref_events, key=lambda e: e['name']): + + chunks = [ + ref.get('module'), + ref.get('class'), + ] + chunks = filter(None, chunks) + prefix = '.'.join(chunks) + '.' if chunks else '' + + if ref.get('property'): + ref_pairs.append((ref['property'], ':attr:`%s%s`' % (prefix, ref['property']))) + elif ref.get('function'): + name = ref['function'] + if name in ('__init__', '__cinit__', '__dealloc__'): + ref_pairs.append((name, ':class:`%s%s <%s>`' % (prefix, name, prefix.rstrip('.')))) + else: + ref_pairs.append((name, ':func:`%s%s`' % (prefix, name))) + else: + continue + + unique_refs = event['_references'] = [] + seen = set() + for name, ref in sorted(ref_pairs): + if name in seen: + continue + seen.add(name) + unique_refs.append(ref) + + + + +print(''' + +.. + This file is generated by includes.py; any modifications will be destroyed! + +Wrapped C Types and Functions +============================= + +''') + +for extern, events in sorted(defs_by_extern.items()): + did_header = False + + for event in events: + + headline = event.get('_headline') + comments = event.get('_comments') + refs = event.get('_references', []) + url = event.get('_doxygen_url') + indent = ' ' if event.get('struct') else '' + + if not headline: + continue + if ( + not filter(None, (x.strip() for x in comments if x.strip())) and + not refs and + event['type'] not in ('struct', ) + ): + pass + + if not did_header: + print('``%s``' % extern) + print('-' * (len(extern) + 4)) + print() + did_header = True + + if url: + print() + print(indent + '.. rst-class:: ffmpeg-quicklink') + print() + print(indent + ' `FFmpeg Docs <%s>`__' % url) + + print(indent + headline) + print() + + if comments: + for line in comments: + print(indent + ' ' + line) + print() + + if refs: + print(indent + ' Referenced by: ', end='') + for i, ref in enumerate(refs): + print((', ' if i else '') + ref, end='') + print('.') + + print() diff --git a/docs/development/includes.rst b/docs/development/includes.rst new file mode 100644 index 000000000..6b2c989cb --- /dev/null +++ b/docs/development/includes.rst @@ -0,0 +1,2 @@ + +.. include:: ../_build/rst/development/includes.rst diff --git a/docs/development/license.rst b/docs/development/license.rst new file mode 100644 index 000000000..57cd288f4 --- /dev/null +++ b/docs/development/license.rst @@ -0,0 +1,12 @@ + +.. It is all in the other file (that we want at the top-level of the repo). + +.. _license: + +License +======= + +From `LICENSE.txt `_: + +.. literalinclude:: ../../LICENSE.txt + :language: text diff --git a/docs/generate-tagfile b/docs/generate-tagfile new file mode 100755 index 000000000..1f729de5c --- /dev/null +++ b/docs/generate-tagfile @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +import argparse +import os +import subprocess + + +parser = argparse.ArgumentParser() +parser.add_argument("-l", "--library", required=True) +parser.add_argument("-o", "--output", required=True) +args = parser.parse_args() + +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=args.library) +proc.communicate( + """ + +#@INCLUDE = doc/Doxyfile +GENERATE_TAGFILE = {} +GENERATE_HTML = no +GENERATE_LATEX = no +CASE_SENSE_NAMES = yes +INPUT = libavcodec libavdevice libavfilter libavformat libavresample libavutil libswresample libswscale + +""".format( + output + ).encode() +) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..afeaa60d6 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,107 @@ +**PyAV** Documentation +====================== + +**PyAV** is a Pythonic binding for FFmpeg_. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. + +PyAV is for direct and precise access to your media via containers, streams, packets, codecs, and frames. It exposes a few transformations of that data, and helps you get your data to/from other packages (e.g. Numpy and Pillow). + +This power does come with some responsibility as working with media is horrendously complicated and PyAV can't abstract it away or make all the best decisions for you. If the ``ffmpeg`` command does the job without you bending over backwards, PyAV is likely going to be more of a hindrance than a help. + +But where you can't work without it, PyAV is a critical tool. + +Currently we provide: + +- ``libavformat``: + :class:`containers <.Container>`, + audio/video/subtitle :class:`streams <.Stream>`, + :class:`packets <.Packet>`; + +- ``libavdevice`` (by specifying a format to containers); + +- ``libavcodec``: + :class:`.Codec`, + :class:`.CodecContext`, + :class:`.BitStreamFilterContext`, + audio/video :class:`frames <.Frame>`, + :class:`data planes <.Plane>`, + :class:`subtitles <.Subtitle>`; + +- ``libavfilter``: + :class:`.Filter`, + :class:`.Graph`; + +- ``libswscale``: + :class:`.VideoReformatter`; + +- ``libswresample``: + :class:`.AudioResampler`; + +- and a few more utilities. + +.. _FFmpeg: https://ffmpeg.org/ + + +Basic Demo +---------- + +.. testsetup:: + + path_to_video = common.fate_png() # We don't need a full QT here. + + +.. testcode:: + + import av + + av.logging.set_level(av.logging.VERBOSE) + container = av.open(path_to_video) + + for index, frame in enumerate(container.decode(video=0)): + frame.to_image().save(f"frame-{index:04d}.jpg") + + +Overview +-------- + +.. toctree:: + :glob: + :maxdepth: 2 + + overview/* + + +Cookbook +-------- + +.. toctree:: + :glob: + :maxdepth: 2 + + cookbook/* + + +Reference +--------- + +.. toctree:: + :glob: + :maxdepth: 2 + + api/* + + +Development +----------- + +.. toctree:: + :glob: + :maxdepth: 1 + + development/* + + +Indices and Tables +================== +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst new file mode 100644 index 000000000..43b764e48 --- /dev/null +++ b/docs/overview/caveats.rst @@ -0,0 +1,51 @@ +Caveats +======= + +.. _authority_of_docs: + +Authority of Documentation +-------------------------- + +FFmpeg_ is extremely complex, and the PyAV developers have not been successful in making it 100% clear to themselves in all aspects. Our understanding of how it works and how to work with it is via reading the docs, digging through the source, perfoming experiments, and hearing from users where PyAV isn't doing the right thing. + +Only where this documentation is about the mechanics of PyAV can it be considered authoritative. Anywhere that we discuss something that is actually about the underlying FFmpeg libraries comes with the caveat that we can not always be 100% on it. + +It is, unfortunately, often on the user the understand and deal with the edge cases. We encourage you to bring them to our attention via GitHub_ so that we can try to make PyAV deal with it, but we can't always make it work. + + +Unsupported Features +-------------------- + +Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on Gitter_ or open a feature request on GitHub_ (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. + + +Sub-Interpeters +--------------- + +Since we rely upon C callbacks in a few locations, PyAV is not fully compatible with sub-interpreters. Users have experienced lockups in WSGI web applications, for example. + +This is due to the ``PyGILState_Ensure`` calls made by Cython in a C callback from FFmpeg. If this is called in a thread that was not started by Python, it is very likely to break. There is no current instrumentation to detect such events. + +The two main features that are able to cause lockups are: + +1. Python IO (passing a file-like object to ``av.open``). While this is in theory possible, so far it seems like the callbacks are made in the calling thread, and so are safe. + +2. Logging. As soon as you en/decode with threads you are highly likely to get log messages issues from threads started by FFmpeg, and you will get lockups. See :ref:`disable_logging`. + + +.. _garbage_collection: + +Garbage Collection +------------------ + +PyAV currently has a number of reference cycles that make it more difficult for the garbage collector than we would like. In some circumstances (usually tight loops involving opening many containers), a :class:`.Container` will not auto-close until many a few thousand have built-up. + +Until we resolve this issue, you should explicitly call :meth:`.Container.close` or use the container as a context manager:: + + with av.open(path) as fh: + # Do stuff with it. + + +.. _FFmpeg: https://ffmpeg.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 new file mode 100644 index 000000000..c43504d7a --- /dev/null +++ b/docs/overview/installation.rst @@ -0,0 +1,142 @@ +Installation +============ + +Binary wheels +------------- + +Binary wheels are provided on PyPI for Linux, MacOS, and Windows linked against FFmpeg. The most straight-forward way to install PyAV is to run: + +.. code-block:: bash + + pip install av + + +Currently FFmpeg 6.1.1 is used with the following features enabled for all platforms: + +- fontconfig +- gmp +- libaom +- libass +- libbluray +- libdav1d +- libfreetype +- libmp3lame +- libopencore-amrnb +- libopencore-amrwb +- libopenjpeg +- libopus +- libspeex +- libtwolame +- libvorbis +- libvpx +- libx264 +- libx265 +- libxml2 +- libxvid +- lzma +- zlib + +The following additional features are also enabled on Linux: + +- gnutls +- libxcb + + +Conda +----- + +Another way to install PyAV is via `conda-forge `_:: + + conda install av -c conda-forge + +See the `Conda quick install `_ docs to get started with (mini)Conda. + + +Bring your own FFmpeg +--------------------- + +PyAV can also be compiled against your own build of FFmpeg ((version ``5.0`` or higher). You can force installing PyAV from source by running: + +.. code-block:: bash + + pip install av --no-binary av + +PyAV depends upon several libraries from FFmpeg: + +- ``libavcodec`` +- ``libavdevice`` +- ``libavfilter`` +- ``libavformat`` +- ``libavutil`` +- ``libswresample`` +- ``libswscale`` + +and a few other tools in general: + +- ``pkg-config`` +- Python's development headers + + +MacOS +^^^^^ + +On **MacOS**, Homebrew_ saves the day:: + + brew install ffmpeg pkg-config + +.. _homebrew: http://brew.sh/ + + +Ubuntu >= 18.04 LTS +^^^^^^^^^^^^^^^^^^^ + +On **Ubuntu 18.04 LTS** everything can come from the default sources:: + + # General dependencies + sudo apt-get install -y python-dev pkg-config + + # Library components + sudo apt-get install -y \ + libavformat-dev libavcodec-dev libavdevice-dev \ + libavutil-dev libswscale-dev libswresample-dev libavfilter-dev + + +Windows +^^^^^^^ + +It is possible to build PyAV on Windows without Conda by installing FFmpeg yourself, e.g. from the `shared and dev packages `_. + +Unpack them somewhere (like ``C:\ffmpeg``), and then :ref:`tell PyAV where they are located `. + + +Building from the latest source +------------------------------- + +:: + + # Get PyAV from GitHub. + git clone https://github.com/PyAV-Org/PyAV.git + cd PyAV + + # Prep a virtualenv. + source scripts/activate.sh + + # Install basic requirements. + pip install -r tests/requirements.txt + + # Optionally build FFmpeg. + ./scripts/build-deps + + # Build PyAV. + make + +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 + + +.. _build_on_windows: + +On **Windows** you must indicate the location of your FFmpeg, e.g.:: + + python setup.py build --ffmpeg-dir=C:\ffmpeg diff --git a/setup.py b/setup.py index bc13c8cfb..85da5f5e3 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,6 @@ from Cython.Compiler.AutoDocTransforms import EmbedSignature from setuptools import Extension, find_packages, setup - FFMPEG_LIBRARIES = [ "avformat", "avcodec", @@ -72,21 +71,22 @@ def get_config_from_pkg_config(): """ Get distutils-compatible extension arguments using pkg-config. """ + pkg_config = os.environ.get("PKG_CONFIG", "pkg-config") try: raw_cflags = subprocess.check_output( - ["pkg-config", "--cflags", "--libs"] + [pkg_config, "--cflags", "--libs"] + ["lib" + name for name in FFMPEG_LIBRARIES] ) except FileNotFoundError: - print("pkg-config is required for building PyAV") + print(f"{pkg_config} is required for building PyAV") exit(1) except subprocess.CalledProcessError: - print(f"pkg-config could not find libraries {FFMPEG_LIBRARIES}") + print(f"{pkg_config} could not find libraries {FFMPEG_LIBRARIES}") exit(1) known, unknown = parse_cflags(raw_cflags.decode("utf-8")) if unknown: - print(f"pkg-config returned flags we don't understand: {unknown}") + print("pkg-config returned flags we don't understand: {}".format(unknown)) if "-pthread" in unknown: print("Building PyAV against static FFmpeg libraries is not supported.") exit(1) @@ -109,7 +109,7 @@ def parse_cflags(raw_flags): parts = x.split("=", 1) value = x[1] or None if len(x) == 2 else None config["define_macros"][i] = (parts[0], value) - return config, shlex.join(unknown) + return config, " ".join(shlex.quote(x) for x in unknown) # Parse command-line arguments. @@ -195,7 +195,7 @@ def parse_cflags(raw_flags): long_description_content_type="text/markdown", license="BSD", project_urls={ - "Bug Reports": "https://github.com/WyattBlue/pyav/issues", + "Bug Reports": "https://github.com/PyAV-Org/PyAV/issues", "Documentation": "https://pyav.basswood-io.com", "Download": "https://pypi.org/project/pyav", }, @@ -204,31 +204,30 @@ def parse_cflags(raw_flags): url="https://github.com/WyattBlue/pyav", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, + python_requires=">=3.10", zip_safe=False, ext_modules=ext_modules, test_suite="tests", - python_requires=">=3.10", + entry_points={ + "console_scripts": ["pyav = av.__main__:main"], + }, classifiers=[ - "Topic :: Multimedia :: Sound/Audio", - "Topic :: Multimedia :: Sound/Audio :: Conversion", - "Topic :: Multimedia :: Video", - "Topic :: Multimedia :: Video :: Conversion", - "Topic :: Software Development :: Libraries :: Python Modules", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", - "Intended Audience :: Developers", - "Development Status :: 5 - Production/Stable", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Multimedia :: Sound/Audio :: Conversion", + "Topic :: Multimedia :: Video", + "Topic :: Multimedia :: Video :: Conversion", ], - entry_points={ - "console_scripts": ["pyav = av.__main__:main"], - }, )