mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Let SceneFileWriter access ffmpeg via av instead of via external process (#3501)
* added av as a dependency * make partial movie files use av instead of piping to external ffmpeg * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * opengl rendering: use av for movie files * no need to check for ffmpeg executable * refactor: *_movie_pipe -> *_partial_movie_stream * improve (oneline) documentation * pass more options to partial movie file rendering * move ffmpeg verbosity settings to config; renamed option dict * replaced call to ffmpeg in combine_files by using av Co-authored-by: Jérome Eertmans <jeertmans@icloud.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * there was one examples saved as a gif? * chore(deps): re-order av * chore(lib): simplify `write_frame` method Reduces the overall code complexity * chore(lib): add audio * fix(lib): same issue for conversion * fix(lib): webm export * fix(lib): transparent export Though the output video is weird * try(lib): fix gif + TODOs * chore(deps): lower dep crit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat(lib): add support for GIF * fix(ci): rewrite tests * fix * chore(ci): prevent calling concat on empty list * add missing dot * fix(ci): update frame comparison ? * fix(log): add handler to libav logger * chore: add TODO * fix(lib): concat issue * Revert "fix(ci): update frame comparison ?" This reverts commit904cfb46ae. * fix(ci): make it pass tests * chore(lib/docs/ci): remove FFMPEG entirely This removes any reference to FFMPEG, except in translation files * added av as a dependency * make partial movie files use av instead of piping to external ffmpeg * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * opengl rendering: use av for movie files * no need to check for ffmpeg executable * refactor: *_movie_pipe -> *_partial_movie_stream * improve (oneline) documentation * pass more options to partial movie file rendering * move ffmpeg verbosity settings to config; renamed option dict * replaced call to ffmpeg in combine_files by using av Co-authored-by: Jérome Eertmans <jeertmans@icloud.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * there was one examples saved as a gif? * chore(deps): re-order av * chore(lib): simplify `write_frame` method Reduces the overall code complexity * chore(lib): add audio * fix(lib): same issue for conversion * fix(lib): webm export * fix(lib): transparent export Though the output video is weird * try(lib): fix gif + TODOs * chore(deps): lower dep crit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat(lib): add support for GIF * fix(ci): rewrite tests * fix * chore(ci): prevent calling concat on empty list * add missing dot * fix(ci): update frame comparison ? * fix(log): add handler to libav logger * chore: add TODO * fix(lib): concat issue * Revert "fix(ci): update frame comparison ?" This reverts commit904cfb46ae. * fix(ci): make it pass tests * chore(lib/docs/ci): remove FFMPEG entirely This removes any reference to FFMPEG, except in translation files * chore(deps): update lockfile * chore(lib): rewrite ffprobe * fix typo * slightly more aggressive removal of ffmpeg in docs; minor language changes * fix gif output stream dimensions * minor style change * fix encoding of (transparent) mov files * fixed metadata / comment * set frame rate for --format=gif in output_stream * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * more video tests for different render settings, also test pix_fmt * improve default bitrate setting via crf * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * parametrized format/transparency rendering test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * context managers for (some) av.open * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update manim/utils/commands.py Co-authored-by: Jérome Eertmans <jeertmans@icloud.com> * fixed segfault * update test data involving implicit functions (output improved!) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * explicity set pix_fmt for transparent webms * special-special case extracting frame from vp9-encoded file with transparency * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix transparent gifs, more special casing in parametrized video format test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * run tests on macos-latest again * removed old control data * Revert "run tests on macos-latest again" This reverts commitf50efa4b88. * added sound to codec test; fixed issue with sound track in gif (disabled) and webm (now via opus) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * manual wav -> ogg transcoding * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fixed f-string * refactored codec test, split out gif * check for non-zero audio samples * more cleanup * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove ffmpeg from readthedocs apt_packages * round up run_time if positive and shorter than current frame rate * added more run_time tests * black * improve implementation of test * removed some unused imports * improve wording of logged warning Co-authored-by: Jérome Eertmans <jeertmans@icloud.com> * move run_time checks from Animation.begin to Scene.get_run_time * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove unused import * flake: PT012 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
This commit is contained in:
parent
4bd2f860b4
commit
1f249e45b9
54 changed files with 1868 additions and 1398 deletions
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -70,14 +70,5 @@ PASTE HERE
|
|||
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
|
||||
</details>
|
||||
|
||||
<details><summary>FFMPEG</summary>
|
||||
|
||||
Output of `ffmpeg -version`:
|
||||
|
||||
```
|
||||
PASTE HERE
|
||||
```
|
||||
</details>
|
||||
|
||||
## Additional comments
|
||||
<!-- Add further context that you think might be relevant for this issue here. -->
|
||||
|
|
|
|||
9
.github/ISSUE_TEMPLATE/installation_issue.md
vendored
9
.github/ISSUE_TEMPLATE/installation_issue.md
vendored
|
|
@ -53,14 +53,5 @@ PASTE HERE
|
|||
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
|
||||
</details>
|
||||
|
||||
<details><summary>FFMPEG</summary>
|
||||
|
||||
Output of `ffmpeg -version`:
|
||||
|
||||
```
|
||||
PASTE HERE
|
||||
```
|
||||
</details>
|
||||
|
||||
## Additional comments
|
||||
<!-- Add further context that you think might be relevant for this issue here. -->
|
||||
|
|
|
|||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -51,12 +51,6 @@ jobs:
|
|||
run: |
|
||||
echo "date=$(/bin/date -u "+%m%w%Y")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install and cache ffmpeg (all OS)
|
||||
uses: FedericoCarboni/setup-ffmpeg@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
id: setup-ffmpeg
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y \
|
||||
pkg-config libcairo-dev libpango1.0-dev ffmpeg wget fonts-roboto
|
||||
pkg-config libcairo-dev libpango1.0-dev wget fonts-roboto
|
||||
wget -qO- "https://yihui.org/tinytex/install-bin-unix.sh" | sh
|
||||
echo ${HOME}/.TinyTeX/bin/x86_64-linux >> $GITHUB_PATH
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ build:
|
|||
|
||||
apt_packages:
|
||||
- libpango1.0-dev
|
||||
- ffmpeg
|
||||
- graphviz
|
||||
|
||||
python:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ FROM python:3.11-slim
|
|||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
ffmpeg \
|
||||
build-essential \
|
||||
gcc \
|
||||
cmake \
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ For first-time contributors
|
|||
#. Install Manim:
|
||||
|
||||
- Follow the steps in our :doc:`installation instructions
|
||||
<../installation>` to install **Manim's dependencies**,
|
||||
primarily ``ffmpeg`` and ``LaTeX``.
|
||||
<../installation>` to install **Manim's system dependencies**.
|
||||
We also recommend installing a LaTeX distribution.
|
||||
|
||||
- We recommend using `Poetry <https://python-poetry.org>`__ to manage your
|
||||
developer installation of Manim. Poetry is a tool for dependency
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ The scene then asks its renderer to initialize the scene by calling
|
|||
|
||||
Inspecting both the default Cairo renderer and the OpenGL renderer shows that the ``init_scene``
|
||||
method effectively makes the renderer instantiate a :class:`.SceneFileWriter` object, which
|
||||
basically is Manim's interface to ``ffmpeg`` and actually writes the movie file. The Cairo
|
||||
basically is Manim's interface to ``libav`` (FFMPEG) and actually writes the movie file. The Cairo
|
||||
renderer (see the implementation `here <https://github.com/ManimCommunity/manim/blob/main/manim/renderer/cairo_renderer.py>`__) does not require any further initialization. The OpenGL renderer
|
||||
does some additional setup to enable the realtime rendering preview window, which we do not go
|
||||
into detail further here.
|
||||
|
|
@ -310,8 +310,8 @@ the order they are called, these customizable methods are:
|
|||
After these three methods are run, the animations have been fully rendered,
|
||||
and Manim calls :meth:`.CairoRenderer.scene_finished` to gracefully
|
||||
complete the rendering process. This checks whether any animations have been
|
||||
played -- and if so, it tells the :class:`.SceneFileWriter` to close the pipe
|
||||
to ``ffmpeg``. If not, Manim assumes that a static image should be output
|
||||
played -- and if so, it tells the :class:`.SceneFileWriter` to close the output
|
||||
file. If not, Manim assumes that a static image should be output
|
||||
which it then renders using the same strategy by calling the render loop
|
||||
(see below) once.
|
||||
|
||||
|
|
@ -762,10 +762,10 @@ to learn more, the :func:`.get_hash_from_play_call` function in the
|
|||
mechanism.
|
||||
|
||||
In the event that the animation has to be rendered, the renderer asks
|
||||
its :class:`.SceneFileWriter` to start a writing process. The process
|
||||
is started by a call to ``ffmpeg`` and opens a pipe to which rendered
|
||||
raw frames can be written. As long as the pipe is open, the process
|
||||
can be accessed via the ``writing_process`` attribute of the file writer.
|
||||
its :class:`.SceneFileWriter` to open an output container. The process
|
||||
is started by a call to ``libav`` and opens a container to which rendered
|
||||
raw frames can be written. As long as the output is open, the container
|
||||
can be accessed via the ``output_container`` attribute of the file writer.
|
||||
With the writing process in place, the renderer then asks the scene
|
||||
to "begin" the animations.
|
||||
|
||||
|
|
@ -815,7 +815,7 @@ time is extracted (3 seconds long) and stored in
|
|||
skip (it should not), then whether the animation is already
|
||||
cached (it is not). The corresponding animation hash value is
|
||||
determined and passed to the file writer, which then also calls
|
||||
``ffmpeg`` to start the writing process which waits for rendered
|
||||
``libav`` to start the writing process which waits for rendered
|
||||
frames from the library.
|
||||
|
||||
The scene then ``begin``\ s the animation: for the
|
||||
|
|
@ -1001,7 +1001,7 @@ and :meth:`.Animation.clean_up_from_scene` methods are called.
|
|||
In the end, the time progression is closed (which completes the displayed progress bar)
|
||||
in the terminal. With the closing of the time progression, the
|
||||
:meth:`.Scene.play_internal` call is completed, and we return to the renderer,
|
||||
which now orders the :class:`.SceneFileWriter` to close the movie pipe that has
|
||||
which now orders the :class:`.SceneFileWriter` to close the output container that has
|
||||
been opened for this animation: a partial movie file is written.
|
||||
|
||||
This pretty much concludes the walkthrough of a :class:`.Scene.play` call,
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ Installing Manim via Conda and related environment managers
|
|||
Conda is a package manager for Python that allows creating environments
|
||||
where all your dependencies are stored. Like this, you don't clutter up your PC with
|
||||
unwanted libraries and you can just delete the environment when you don't need it anymore.
|
||||
It is a good way to install manim since all dependencies like
|
||||
``ffmpeg``, ``pycairo``, etc. come with it.
|
||||
It is a good way to install manim since all dependencies like ``pycairo``, etc. come with it.
|
||||
Also, the installation steps are the same, no matter if you are
|
||||
on Windows, Linux, Intel Macs or on Apple Silicon.
|
||||
|
||||
|
|
@ -69,7 +68,7 @@ in order for Manim to work properly, some additional system
|
|||
dependencies need to be installed first. The following pages have
|
||||
operating system specific instructions for you to follow.
|
||||
|
||||
Manim requires Python version ``3.8`` or above to run.
|
||||
Manim requires Python version ``3.9`` or above to run.
|
||||
|
||||
.. hint::
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ then execute it.
|
|||
.. code-block::
|
||||
|
||||
!sudo apt update
|
||||
!sudo apt install libcairo2-dev ffmpeg \
|
||||
!sudo apt install libcairo2-dev \
|
||||
texlive texlive-latex-extra texlive-fonts-extra \
|
||||
texlive-latex-recommended texlive-science \
|
||||
tipa libpango1.0-dev
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ The installation instructions depend on your particular operating
|
|||
system and package manager. If you happen to know exactly what you are doing,
|
||||
you can also simply ensure that your system has:
|
||||
|
||||
- a reasonably recent version of Python 3 (3.8 or above),
|
||||
- a reasonably recent version of Python 3 (3.9 or above),
|
||||
- with working Cairo bindings in the form of
|
||||
`pycairo <https://cairographics.org/pycairo/>`__,
|
||||
- FFmpeg accessible from the command line as ``ffmpeg``,
|
||||
- and `Pango <https://pango.gnome.org>`__ headers.
|
||||
|
||||
Then, installing Manim is just a matter of running:
|
||||
|
|
@ -33,13 +32,13 @@ Required Dependencies
|
|||
apt – Ubuntu / Mint / Debian
|
||||
****************************
|
||||
|
||||
To first update your sources, and then install Cairo, Pango, and FFmpeg
|
||||
To first update your sources, and then install Cairo and Pango
|
||||
simply run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt update
|
||||
sudo apt install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg
|
||||
sudo apt install build-essential python3-dev libcairo2-dev libpango1.0-dev
|
||||
|
||||
If you don't have python3-pip installed, install it via:
|
||||
|
||||
|
|
@ -72,14 +71,6 @@ need the Python development headers:
|
|||
|
||||
sudo dnf install python3-devel
|
||||
|
||||
FFmpeg is only available via the RPMfusion repository which you have to
|
||||
configure first – please consult https://rpmfusion.org/Configuration/ for
|
||||
instructions. Then, install FFmpeg:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo dnf install ffmpeg
|
||||
|
||||
At this point you have all required dependencies and can install
|
||||
Manim by running:
|
||||
|
||||
|
|
@ -100,12 +91,12 @@ pacman – Arch / Manjaro
|
|||
|
||||
If you don't want to use the packaged version from AUR, here is what
|
||||
you need to do manually: Update your package sources, then install
|
||||
Cairo, Pango, and FFmpeg:
|
||||
Cairo and Pango:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pacman -Syu
|
||||
sudo pacman -S cairo pango ffmpeg
|
||||
sudo pacman -S cairo pango
|
||||
|
||||
If you don't have ``python-pip`` installed, get it by running:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ follow `Homebrew's installation instructions <https://docs.brew.sh/Installation>
|
|||
Required Dependencies
|
||||
---------------------
|
||||
|
||||
To install all required dependencies for installing Manim (namely: ffmpeg, Python,
|
||||
To install all required dependencies for installing Manim (namely: Python,
|
||||
and some required Python packages), run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install py3cairo ffmpeg
|
||||
brew install py3cairo
|
||||
|
||||
On *Apple Silicon* based machines (i.e., devices with the M1 chip or similar; if
|
||||
you are unsure which processor you have check by opening the Apple menu, select
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@ Windows
|
|||
|
||||
The easiest way of installing Manim and its dependencies is by using a
|
||||
package manager like `Chocolatey <https://chocolatey.org/>`__
|
||||
or `Scoop <https://scoop.sh>`__. If you are not afraid of editing
|
||||
your System's ``PATH``, a manual installation is also possible.
|
||||
In fact, if you already have an existing Python
|
||||
installation (3.8 or above), it might be the easiest way to get
|
||||
everything up and running.
|
||||
or `Scoop <https://scoop.sh>`__, especially if you need optional dependencies
|
||||
like LaTeX support.
|
||||
|
||||
If you choose to use one of the package managers, please follow
|
||||
their installation instructions
|
||||
|
|
@ -19,7 +16,7 @@ to make one of them available on your system.
|
|||
Required Dependencies
|
||||
---------------------
|
||||
|
||||
Manim requires a recent version of Python (3.8 or above) and ``ffmpeg``
|
||||
Manim requires a recent version of Python (3.9 or above)
|
||||
in order to work.
|
||||
|
||||
Chocolatey
|
||||
|
|
@ -34,53 +31,11 @@ Manim can be installed via Chocolatey simply by running:
|
|||
That's it, no further steps required. You can continue with installing
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` below.
|
||||
|
||||
Scoop
|
||||
*****
|
||||
|
||||
While there is no recipe for installing Manim with Scoop directly,
|
||||
you can install all requirements by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
scoop install python ffmpeg
|
||||
|
||||
and then Manim can be installed by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
python -m pip install manim
|
||||
|
||||
Manim should now be installed on your system. Continue reading
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` section
|
||||
below.
|
||||
|
||||
Winget
|
||||
******
|
||||
|
||||
While there is no recipe for installing Manim with Winget directly,
|
||||
you can install all requirements by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
winget install python
|
||||
winget install ffmpeg
|
||||
|
||||
and then Manim can be installed by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
python -m pip install manim
|
||||
|
||||
Manim should now be installed on your system. Continue reading
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` section
|
||||
below.
|
||||
|
||||
|
||||
Manual Installation
|
||||
*******************
|
||||
Pip
|
||||
***
|
||||
|
||||
As mentioned above, Manim needs a reasonably recent version of
|
||||
Python 3 (3.8 or above) and FFmpeg.
|
||||
Python 3 (3.9 or above).
|
||||
|
||||
**Python:** Head over to https://www.python.org, download an installer
|
||||
for a recent version of Python, and follow its instructions to get Python
|
||||
|
|
@ -94,35 +49,16 @@ installed on your system.
|
|||
install Python directly from the
|
||||
`official website <https://www.python.org>`__.
|
||||
|
||||
**FFmpeg:** In order to install FFmpeg, you can get a
|
||||
pre-compiled and ready-to-use version from one of the resources
|
||||
linked at https://ffmpeg.org/download.html#build-windows, such as
|
||||
`the version available here
|
||||
<https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z>`__
|
||||
(recommended), or if you know exactly what you are doing
|
||||
you can alternatively get the source code
|
||||
from https://ffmpeg.org/download.html and compile it yourself.
|
||||
|
||||
|
||||
After downloading the pre-compiled archive,
|
||||
`unzip it <https://www.7-zip.org>`__ and, if you like, move the
|
||||
extracted directory to some more permanent place (e.g.,
|
||||
``C:\Program Files\``). Next, edit the ``PATH`` environment variable:
|
||||
first, visit ``Control Panel`` > ``System`` > ``System settings`` >
|
||||
``Environment Variables``, then add the full path to the ``bin``
|
||||
directory inside of the (moved) ffmpeg directory to the
|
||||
``PATH`` variable. Finally, save your changes and exit.
|
||||
|
||||
If you now open a new command line prompt (or PowerShell) and
|
||||
run ``ffmpeg``, the command should be recognized.
|
||||
|
||||
At this point, you have all the required dependencies and can now
|
||||
install Manim via
|
||||
Then, Manim can be installed via Pip simply by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
python -m pip install manim
|
||||
|
||||
Manim should now be installed on your system. Continue reading
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` section
|
||||
below.
|
||||
|
||||
|
||||
.. _win-optional-dependencies:
|
||||
|
||||
|
|
|
|||
|
|
@ -221,8 +221,6 @@ repr_number = green
|
|||
# Uncomment the following line to manually set the loglevel for ffmpeg. See
|
||||
# ffmpeg manpage for accepted values
|
||||
loglevel = ERROR
|
||||
# defaults to the one present in path
|
||||
ffmpeg_executable = ffmpeg
|
||||
|
||||
[jupyter]
|
||||
media_embed = False
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ def make_logger(
|
|||
logger.addHandler(rich_handler)
|
||||
logger.setLevel(verbosity)
|
||||
|
||||
if not (libav_logger := logging.getLogger()).hasHandlers():
|
||||
libav_logger.addHandler(rich_handler)
|
||||
libav_logger.setLevel(verbosity)
|
||||
|
||||
return logger, console, error_console
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -263,7 +263,6 @@ class ManimConfig(MutableMapping):
|
|||
"dry_run",
|
||||
"enable_wireframe",
|
||||
"ffmpeg_loglevel",
|
||||
"ffmpeg_executable",
|
||||
"format",
|
||||
"flush_cache",
|
||||
"frame_height",
|
||||
|
|
@ -679,10 +678,6 @@ class ManimConfig(MutableMapping):
|
|||
if val:
|
||||
self.ffmpeg_loglevel = val
|
||||
|
||||
# TODO: Fix the mess above and below
|
||||
val = parser["ffmpeg"].get("ffmpeg_executable")
|
||||
setattr(self, "ffmpeg_executable", val)
|
||||
|
||||
try:
|
||||
val = parser["jupyter"].getboolean("media_embed")
|
||||
except ValueError:
|
||||
|
|
@ -1074,15 +1069,7 @@ class ManimConfig(MutableMapping):
|
|||
val,
|
||||
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
)
|
||||
|
||||
@property
|
||||
def ffmpeg_executable(self) -> str:
|
||||
"""Custom path to the ffmpeg executable."""
|
||||
return self._d["ffmpeg_executable"]
|
||||
|
||||
@ffmpeg_executable.setter
|
||||
def ffmpeg_executable(self, value: str) -> None:
|
||||
self._set_str("ffmpeg_executable", value)
|
||||
logging.getLogger("libav").setLevel(self.ffmpeg_loglevel)
|
||||
|
||||
@property
|
||||
def media_embed(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -192,11 +192,6 @@ class Animation:
|
|||
method.
|
||||
|
||||
"""
|
||||
if self.run_time <= 0:
|
||||
raise ValueError(
|
||||
f"{self} has a run_time of <= 0 seconds, this cannot be rendered correctly. "
|
||||
"Please set the run_time to be positive"
|
||||
)
|
||||
self.starting_mobject = self.create_starting_mobject()
|
||||
if self.suspend_mobject_updating:
|
||||
# All calls to self.mobject's internal updaters
|
||||
|
|
|
|||
|
|
@ -81,16 +81,12 @@ class AnimationGroup(Animation):
|
|||
return list(self.group)
|
||||
|
||||
def begin(self) -> None:
|
||||
if self.run_time <= 0:
|
||||
tmp = (
|
||||
"Please set the run_time to be positive"
|
||||
if len(self.animations) != 0
|
||||
else "Please add at least one Animation with positive run_time"
|
||||
)
|
||||
if not self.animations:
|
||||
raise ValueError(
|
||||
f"{self} has a run_time of 0 seconds, this cannot be "
|
||||
f"rendered correctly. {tmp}."
|
||||
f"Trying to play {self} without animations, this is not supported. "
|
||||
"Please add at least one subanimation."
|
||||
)
|
||||
|
||||
self.anim_group_time = 0.0
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.suspend_updating()
|
||||
|
|
@ -233,7 +229,11 @@ class Succession(AnimationGroup):
|
|||
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
|
||||
|
||||
def begin(self) -> None:
|
||||
assert len(self.animations) > 0
|
||||
if not self.animations:
|
||||
raise ValueError(
|
||||
f"Trying to play {self} without animations, this is not supported. "
|
||||
"Please add at least one subanimation."
|
||||
)
|
||||
self.update_active_animation(0)
|
||||
|
||||
def finish(self) -> None:
|
||||
|
|
|
|||
|
|
@ -104,45 +104,6 @@ def is_manim_executable_associated_to_this_library():
|
|||
return b"manim.__main__" in manim_exec or b'"%~dp0\\manim"' in manim_exec
|
||||
|
||||
|
||||
@healthcheck(
|
||||
description="Checking whether ffmpeg is available",
|
||||
recommendation=(
|
||||
"Manim does not work without ffmpeg. Please follow our "
|
||||
"installation instructions "
|
||||
"at https://docs.manim.community/en/stable/installation.html "
|
||||
"to download ffmpeg. Then, either ...\n\n"
|
||||
"(a) ... make the ffmpeg executable available to your system's PATH,\n"
|
||||
"(b) or, alternatively, use <manim cfg write --open> to create a "
|
||||
"custom configuration and set the ffmpeg_executable variable to the "
|
||||
"full absolute path to the ffmpeg executable."
|
||||
),
|
||||
)
|
||||
def is_ffmpeg_available():
|
||||
path_to_ffmpeg = shutil.which(config.ffmpeg_executable)
|
||||
return path_to_ffmpeg is not None and os.access(path_to_ffmpeg, os.X_OK)
|
||||
|
||||
|
||||
@healthcheck(
|
||||
description="Checking whether ffmpeg is working",
|
||||
recommendation=(
|
||||
"Your installed version of ffmpeg does not support x264 encoding, "
|
||||
"which manim requires. Please follow our installation instructions "
|
||||
"at https://docs.manim.community/en/stable/installation.html "
|
||||
"to download and install a newer version of ffmpeg."
|
||||
),
|
||||
skip_on_failed=[is_ffmpeg_available],
|
||||
)
|
||||
def is_ffmpeg_working():
|
||||
ffmpeg_version = subprocess.run(
|
||||
[config.ffmpeg_executable, "-version"],
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.decode()
|
||||
return (
|
||||
ffmpeg_version.startswith("ffmpeg version")
|
||||
and "--enable-libx264" in ffmpeg_version
|
||||
)
|
||||
|
||||
|
||||
@healthcheck(
|
||||
description="Checking whether latex is available",
|
||||
recommendation=(
|
||||
|
|
|
|||
|
|
@ -472,7 +472,6 @@ class Star(Polygon):
|
|||
Examples
|
||||
--------
|
||||
.. manim:: StarExample
|
||||
:save_as_gif:
|
||||
|
||||
class StarExample(Scene):
|
||||
def construct(self):
|
||||
|
|
|
|||
|
|
@ -186,8 +186,7 @@ class CairoRenderer:
|
|||
if self.skip_animations:
|
||||
return
|
||||
self.time += num_frames * dt
|
||||
for _ in range(num_frames):
|
||||
self.file_writer.write_frame(frame)
|
||||
self.file_writer.write_frame(frame, num_frames=num_frames)
|
||||
|
||||
def freeze_current_frame(self, duration: float):
|
||||
"""Adds a static frame to the movie for a given duration. The static frame is the current frame.
|
||||
|
|
|
|||
|
|
@ -425,8 +425,9 @@ class OpenGLRenderer:
|
|||
self.update_frame(scene)
|
||||
|
||||
if not self.skip_animations:
|
||||
for _ in range(int(config.frame_rate * scene.duration)):
|
||||
self.file_writer.write_frame(self)
|
||||
self.file_writer.write_frame(
|
||||
self, num_frames=int(config.frame_rate * scene.duration)
|
||||
)
|
||||
|
||||
if self.window is not None:
|
||||
self.window.swap_buffers()
|
||||
|
|
|
|||
|
|
@ -1030,12 +1030,28 @@ class Scene:
|
|||
float
|
||||
The total ``run_time`` of all of the animations in the list.
|
||||
"""
|
||||
max_run_time = 0
|
||||
frame_rate = (
|
||||
1 / config.frame_rate
|
||||
) # config.frame_rate holds the number of frames per second
|
||||
for animation in animations:
|
||||
if animation.run_time <= 0:
|
||||
raise ValueError(
|
||||
f"{animation} has a run_time of <= 0 seconds which Manim cannot render. "
|
||||
"Please set the run_time to be positive."
|
||||
)
|
||||
elif animation.run_time < frame_rate:
|
||||
logger.warning(
|
||||
f"Original run time of {animation} is shorter than current frame "
|
||||
f"rate (1 frame every {frame_rate:.2f} sec.) which cannot be rendered. "
|
||||
"Rendering with the shortest possible duration instead."
|
||||
)
|
||||
animation.run_time = frame_rate
|
||||
|
||||
if len(animations) == 1 and isinstance(animations[0], Wait):
|
||||
return animations[0].duration
|
||||
if animation.run_time > max_run_time:
|
||||
max_run_time = animation.run_time
|
||||
|
||||
else:
|
||||
return np.max([animation.run_time for animation in animations])
|
||||
return max_run_time
|
||||
|
||||
def play(
|
||||
self,
|
||||
|
|
@ -1205,16 +1221,16 @@ class Scene:
|
|||
self.moving_mobjects = []
|
||||
self.static_mobjects = []
|
||||
|
||||
self.duration = self.get_run_time(self.animations)
|
||||
if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
|
||||
if self.should_update_mobjects():
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
self.stop_condition = self.animations[0].stop_condition
|
||||
else:
|
||||
self.duration = self.animations[0].duration
|
||||
# Static image logic when the wait is static is done by the renderer, not here.
|
||||
self.animations[0].is_static_wait = True
|
||||
return None
|
||||
self.duration = self.get_run_time(self.animations)
|
||||
|
||||
return self
|
||||
|
||||
def begin_animations(self) -> None:
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ from __future__ import annotations
|
|||
__all__ = ["SceneFileWriter"]
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import srt
|
||||
from PIL import Image
|
||||
|
|
@ -24,11 +23,9 @@ from ..constants import RendererType
|
|||
from ..utils.file_ops import (
|
||||
add_extension_if_not_present,
|
||||
add_version_before_extension,
|
||||
ensure_executable,
|
||||
guarantee_existence,
|
||||
is_gif_format,
|
||||
is_png_format,
|
||||
is_webm_format,
|
||||
modify_atime,
|
||||
write_to_movie,
|
||||
)
|
||||
|
|
@ -83,14 +80,6 @@ class SceneFileWriter:
|
|||
self.next_section(
|
||||
name="autocreated", type=DefaultSectionType.NORMAL, skip_animations=False
|
||||
)
|
||||
# fail fast if ffmpeg is not found
|
||||
if not ensure_executable(Path(config.ffmpeg_executable)):
|
||||
raise RuntimeError(
|
||||
"Manim could not find ffmpeg, which is required for generating video output.\n"
|
||||
"For installing ffmpeg please consult https://docs.manim.community/en/stable/installation.html\n"
|
||||
"Make sure to either add ffmpeg to the PATH environment variable\n"
|
||||
"or set path to the ffmpeg executable under the ffmpeg header in Manim's configuration."
|
||||
)
|
||||
|
||||
def init_output_directories(self, scene_name):
|
||||
"""Initialise output directories.
|
||||
|
|
@ -358,7 +347,7 @@ class SceneFileWriter:
|
|||
Whether or not to write to a video file.
|
||||
"""
|
||||
if write_to_movie() and allow_write:
|
||||
self.open_movie_pipe(file_path=file_path)
|
||||
self.open_partial_movie_stream(file_path=file_path)
|
||||
|
||||
def end_animation(self, allow_write: bool = False):
|
||||
"""
|
||||
|
|
@ -371,9 +360,11 @@ class SceneFileWriter:
|
|||
Whether or not to write to a video file.
|
||||
"""
|
||||
if write_to_movie() and allow_write:
|
||||
self.close_movie_pipe()
|
||||
self.close_partial_movie_stream()
|
||||
|
||||
def write_frame(self, frame_or_renderer: np.ndarray | OpenGLRenderer):
|
||||
def write_frame(
|
||||
self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1
|
||||
):
|
||||
"""
|
||||
Used internally by Manim to write a frame to
|
||||
the FFMPEG input buffer.
|
||||
|
|
@ -382,41 +373,41 @@ class SceneFileWriter:
|
|||
----------
|
||||
frame_or_renderer
|
||||
Pixel array of the frame.
|
||||
num_frames
|
||||
The number of times to write frame.
|
||||
"""
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
self.write_opengl_frame(frame_or_renderer)
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
frame = frame_or_renderer
|
||||
if write_to_movie():
|
||||
self.writing_process.stdin.write(frame.tobytes())
|
||||
if is_png_format() and not config["dry_run"]:
|
||||
self.output_image_from_array(frame)
|
||||
|
||||
def write_opengl_frame(self, renderer: OpenGLRenderer):
|
||||
if write_to_movie():
|
||||
self.writing_process.stdin.write(
|
||||
renderer.get_raw_frame_buffer_object_data(),
|
||||
frame: np.ndarray = (
|
||||
frame_or_renderer.get_frame()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else frame_or_renderer
|
||||
)
|
||||
for _ in range(num_frames):
|
||||
# Notes: precomputing reusing packets does not work!
|
||||
# I.e., you cannot do `packets = encode(...)`
|
||||
# and reuse it, as it seems that `mux(...)`
|
||||
# consumes the packet.
|
||||
# The same issue applies for `av_frame`,
|
||||
# reusing it renders weird-looking frames.
|
||||
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
|
||||
for packet in self.video_stream.encode(av_frame):
|
||||
self.video_container.mux(packet)
|
||||
|
||||
if is_png_format() and not config["dry_run"]:
|
||||
image: Image = (
|
||||
frame_or_renderer.get_image()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else Image.fromarray(frame_or_renderer)
|
||||
)
|
||||
elif is_png_format() and not config["dry_run"]:
|
||||
target_dir = self.image_file_path.parent / self.image_file_path.stem
|
||||
extension = self.image_file_path.suffix
|
||||
self.output_image(
|
||||
renderer.get_image(),
|
||||
image,
|
||||
target_dir,
|
||||
extension,
|
||||
config["zero_pad"],
|
||||
)
|
||||
|
||||
def output_image_from_array(self, frame_data):
|
||||
target_dir = self.image_file_path.parent / self.image_file_path.stem
|
||||
extension = self.image_file_path.suffix
|
||||
self.output_image(
|
||||
Image.fromarray(frame_data),
|
||||
target_dir,
|
||||
extension,
|
||||
config["zero_pad"],
|
||||
)
|
||||
|
||||
def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool):
|
||||
if zero_pad:
|
||||
image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}")
|
||||
|
|
@ -467,11 +458,11 @@ class SceneFileWriter:
|
|||
if self.subcaptions:
|
||||
self.write_subcaption_file()
|
||||
|
||||
def open_movie_pipe(self, file_path=None):
|
||||
"""
|
||||
Used internally by Manim to initialise
|
||||
FFMPEG and begin writing to FFMPEG's input
|
||||
buffer.
|
||||
def open_partial_movie_stream(self, file_path=None):
|
||||
"""Open a container holding a video stream.
|
||||
|
||||
This is used internally by Manim initialize the container holding
|
||||
the video stream of a partial movie file.
|
||||
"""
|
||||
if file_path is None:
|
||||
file_path = self.partial_movie_files[self.renderer.num_plays]
|
||||
|
|
@ -480,49 +471,48 @@ class SceneFileWriter:
|
|||
fps = config["frame_rate"]
|
||||
if fps == int(fps): # fps is integer
|
||||
fps = int(fps)
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
width, height = self.renderer.get_pixel_shape()
|
||||
else:
|
||||
height = config["pixel_height"]
|
||||
width = config["pixel_width"]
|
||||
|
||||
command = [
|
||||
config.ffmpeg_executable,
|
||||
"-y", # overwrite output file if it exists
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-s",
|
||||
"%dx%d" % (width, height), # size of one frame
|
||||
"-pix_fmt",
|
||||
"rgba",
|
||||
"-r",
|
||||
str(fps), # frames per second
|
||||
"-i",
|
||||
"-", # The input comes from a pipe
|
||||
"-an", # Tells FFMPEG not to expect any audio
|
||||
"-loglevel",
|
||||
config["ffmpeg_loglevel"].lower(),
|
||||
"-metadata",
|
||||
f"comment=Rendered with Manim Community v{__version__}",
|
||||
]
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
command += ["-vf", "vflip"]
|
||||
if is_webm_format():
|
||||
command += ["-vcodec", "libvpx-vp9", "-auto-alt-ref", "0"]
|
||||
# .mov format
|
||||
elif config["transparent"]:
|
||||
command += ["-vcodec", "qtrle"]
|
||||
else:
|
||||
command += ["-vcodec", "libx264", "-pix_fmt", "yuv420p"]
|
||||
command += [file_path]
|
||||
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
||||
partial_movie_file_codec = "libx264"
|
||||
partial_movie_file_pix_fmt = "yuv420p"
|
||||
av_options = {
|
||||
"an": "1", # ffmpeg: -an, no audio
|
||||
"crf": "23", # ffmpeg: -crf, constant rate factor (improved bitrate)
|
||||
}
|
||||
|
||||
def close_movie_pipe(self):
|
||||
if config.format == "webm":
|
||||
partial_movie_file_codec = "libvpx-vp9"
|
||||
av_options["-auto-alt-ref"] = "1"
|
||||
if config.transparent:
|
||||
partial_movie_file_pix_fmt = "yuva420p"
|
||||
|
||||
elif config.transparent:
|
||||
partial_movie_file_codec = "qtrle"
|
||||
partial_movie_file_pix_fmt = "argb"
|
||||
|
||||
with av.open(file_path, mode="w") as video_container:
|
||||
stream = video_container.add_stream(
|
||||
partial_movie_file_codec,
|
||||
rate=config.frame_rate,
|
||||
options=av_options,
|
||||
)
|
||||
stream.pix_fmt = partial_movie_file_pix_fmt
|
||||
stream.width = config.pixel_width
|
||||
stream.height = config.pixel_height
|
||||
|
||||
self.video_container = video_container
|
||||
self.video_stream = stream
|
||||
|
||||
def close_partial_movie_stream(self):
|
||||
"""Close the currently opened video container.
|
||||
|
||||
Used internally by Manim to first flush the remaining packages
|
||||
in the video stream holding a partial file, and then close
|
||||
the corresponding container.
|
||||
"""
|
||||
Used internally by Manim to gracefully stop writing to FFMPEG's input buffer
|
||||
"""
|
||||
self.writing_process.stdin.close()
|
||||
self.writing_process.wait()
|
||||
for packet in self.video_stream.encode():
|
||||
self.video_container.mux(packet)
|
||||
|
||||
self.video_container.close()
|
||||
|
||||
logger.info(
|
||||
f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s",
|
||||
|
|
@ -567,37 +557,92 @@ class SceneFileWriter:
|
|||
for pf_path in input_files:
|
||||
pf_path = Path(pf_path).as_posix()
|
||||
fp.write(f"file 'file:{pf_path}'\n")
|
||||
commands = [
|
||||
config.ffmpeg_executable,
|
||||
"-y", # overwrite output file if it exists
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
str(file_list),
|
||||
"-loglevel",
|
||||
config.ffmpeg_loglevel.lower(),
|
||||
"-metadata",
|
||||
f"comment=Rendered with Manim Community v{__version__}",
|
||||
"-nostdin",
|
||||
]
|
||||
|
||||
if create_gif:
|
||||
commands += [
|
||||
"-vf",
|
||||
f"fps={np.clip(config['frame_rate'], 1, 50)},split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle",
|
||||
]
|
||||
else:
|
||||
commands += ["-c", "copy"]
|
||||
av_options = {
|
||||
"safe": "0", # needed to read files
|
||||
}
|
||||
|
||||
if not includes_sound:
|
||||
commands += ["-an"]
|
||||
av_options["an"] = "1"
|
||||
|
||||
commands += [str(output_file)]
|
||||
partial_movies_input = av.open(
|
||||
str(file_list), options=av_options, format="concat"
|
||||
)
|
||||
partial_movies_stream = partial_movies_input.streams.video[0]
|
||||
output_container = av.open(str(output_file), mode="w")
|
||||
output_container.metadata["comment"] = (
|
||||
f"Rendered with Manim Community v{__version__}"
|
||||
)
|
||||
output_stream = output_container.add_stream(
|
||||
codec_name="gif" if create_gif else None,
|
||||
template=partial_movies_stream if not create_gif else None,
|
||||
)
|
||||
if config.transparent and config.format == "webm":
|
||||
output_stream.pix_fmt = "yuva420p"
|
||||
if create_gif:
|
||||
"""
|
||||
The following solution was largely inspired from this comment
|
||||
https://github.com/imageio/imageio/issues/995#issuecomment-1580533018,
|
||||
and the following code
|
||||
https://github.com/imageio/imageio/blob/65d79140018bb7c64c0692ea72cb4093e8d632a0/imageio/plugins/pyav.py#L927-L996.
|
||||
"""
|
||||
output_stream.pix_fmt = "rgb8"
|
||||
if config.transparent:
|
||||
output_stream.pix_fmt = "pal8"
|
||||
output_stream.width = config.pixel_width
|
||||
output_stream.height = config.pixel_height
|
||||
output_stream.rate = config.frame_rate
|
||||
graph = av.filter.Graph()
|
||||
input_buffer = graph.add_buffer(template=partial_movies_stream)
|
||||
split = graph.add("split")
|
||||
palettegen = graph.add("palettegen", "stats_mode=diff")
|
||||
paletteuse = graph.add(
|
||||
"paletteuse", "dither=bayer:bayer_scale=5:diff_mode=rectangle"
|
||||
)
|
||||
output_sink = graph.add("buffersink")
|
||||
|
||||
combine_process = subprocess.Popen(commands)
|
||||
combine_process.wait()
|
||||
input_buffer.link_to(split)
|
||||
split.link_to(palettegen, 0, 0) # 1st input of split -> input of palettegen
|
||||
split.link_to(paletteuse, 1, 0) # 2nd output of split -> 1st input
|
||||
palettegen.link_to(paletteuse, 0, 1) # output of palettegen -> 2nd input
|
||||
paletteuse.link_to(output_sink)
|
||||
|
||||
graph.configure()
|
||||
|
||||
for frame in partial_movies_input.decode(video=0):
|
||||
graph.push(frame)
|
||||
|
||||
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
|
||||
|
||||
frames_written = 0
|
||||
while True:
|
||||
try:
|
||||
frame = graph.pull()
|
||||
frame.time_base = output_stream.codec_context.time_base
|
||||
frame.pts = frames_written
|
||||
frames_written += 1
|
||||
output_container.mux(output_stream.encode(frame))
|
||||
except av.error.EOFError:
|
||||
break
|
||||
|
||||
for packet in output_stream.encode():
|
||||
output_container.mux(packet)
|
||||
|
||||
else:
|
||||
for packet in partial_movies_input.demux(partial_movies_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
packet.dts = None # This seems to be needed, as dts from consecutive
|
||||
# files may not be monotically increasing, so we let libav compute it.
|
||||
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_stream
|
||||
output_container.mux(packet)
|
||||
|
||||
partial_movies_input.close()
|
||||
output_container.close()
|
||||
|
||||
def combine_to_movie(self):
|
||||
"""Used internally by Manim to combine the separate
|
||||
|
|
@ -614,6 +659,10 @@ class SceneFileWriter:
|
|||
movie_file_path = self.movie_file_path
|
||||
if is_gif_format():
|
||||
movie_file_path = self.gif_file_path
|
||||
if len(partial_movie_files) == 0: # Prevent calling concat on empty list
|
||||
logger.info("No animations are contained in this scene.")
|
||||
return
|
||||
|
||||
logger.info("Combining to Movie file.")
|
||||
self.combine_files(
|
||||
partial_movie_files,
|
||||
|
|
@ -623,44 +672,78 @@ class SceneFileWriter:
|
|||
)
|
||||
|
||||
# handle sound
|
||||
if self.includes_sound:
|
||||
if self.includes_sound and config.format != "gif":
|
||||
sound_file_path = movie_file_path.with_suffix(".wav")
|
||||
# Makes sure sound file length will match video file
|
||||
self.add_audio_segment(AudioSegment.silent(0))
|
||||
self.audio_segment.export(
|
||||
sound_file_path,
|
||||
format="wav",
|
||||
bitrate="312k",
|
||||
)
|
||||
# Audio added to a VP9 encoded (webm) video file needs
|
||||
# to be encoded as vorbis or opus. Directly exporting
|
||||
# self.audio_segment with such a codec works in principle,
|
||||
# but tries to call ffmpeg via its CLI -- which we want
|
||||
# to avoid. This is why we need to do the conversion
|
||||
# manually.
|
||||
if config.format == "webm":
|
||||
with (
|
||||
av.open(sound_file_path) as wav_audio,
|
||||
av.open(sound_file_path.with_suffix(".ogg"), "w") as opus_audio,
|
||||
):
|
||||
wav_audio_stream = wav_audio.streams.audio[0]
|
||||
opus_audio_stream = opus_audio.add_stream("libvorbis")
|
||||
for frame in wav_audio.decode(wav_audio_stream):
|
||||
for packet in opus_audio_stream.encode(frame):
|
||||
opus_audio.mux(packet)
|
||||
|
||||
for packet in opus_audio_stream.encode():
|
||||
opus_audio.mux(packet)
|
||||
|
||||
sound_file_path = sound_file_path.with_suffix(".ogg")
|
||||
|
||||
temp_file_path = movie_file_path.with_name(
|
||||
f"{movie_file_path.stem}_temp{movie_file_path.suffix}"
|
||||
)
|
||||
commands = [
|
||||
config.ffmpeg_executable,
|
||||
"-i",
|
||||
str(movie_file_path),
|
||||
"-i",
|
||||
str(sound_file_path),
|
||||
"-y", # overwrite output file if it exists
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"320k",
|
||||
# select video stream from first file
|
||||
"-map",
|
||||
"0:v:0",
|
||||
# select audio stream from second file
|
||||
"-map",
|
||||
"1:a:0",
|
||||
"-loglevel",
|
||||
config.ffmpeg_loglevel.lower(),
|
||||
"-metadata",
|
||||
f"comment=Rendered with Manim Community v{__version__}",
|
||||
# "-shortest",
|
||||
str(temp_file_path),
|
||||
]
|
||||
subprocess.call(commands)
|
||||
av_options = {
|
||||
"shortest": "1",
|
||||
"metadata": f"comment=Rendered with Manim Community v{__version__}",
|
||||
}
|
||||
|
||||
with (
|
||||
av.open(movie_file_path) as video_input,
|
||||
av.open(sound_file_path) as audio_input,
|
||||
):
|
||||
|
||||
video_stream = video_input.streams.video[0]
|
||||
audio_stream = audio_input.streams.audio[0]
|
||||
output_container = av.open(
|
||||
str(temp_file_path), mode="w", options=av_options
|
||||
)
|
||||
output_video_stream = output_container.add_stream(template=video_stream)
|
||||
output_audio_stream = output_container.add_stream(template=audio_stream)
|
||||
|
||||
for packet in video_input.demux(video_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_video_stream
|
||||
output_container.mux(packet)
|
||||
|
||||
for packet in audio_input.demux(audio_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_audio_stream
|
||||
output_container.mux(packet)
|
||||
|
||||
output_container.close()
|
||||
|
||||
shutil.move(str(temp_file_path), str(movie_file_path))
|
||||
sound_file_path.unlink()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
from typing import Generator
|
||||
|
||||
import av
|
||||
|
||||
__all__ = [
|
||||
"capture",
|
||||
"get_video_metadata",
|
||||
|
|
@ -27,21 +28,26 @@ def capture(command, cwd=None, command_input=None):
|
|||
|
||||
|
||||
def get_video_metadata(path_to_video: str | os.PathLike) -> dict[str]:
|
||||
command = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height,nb_frames,duration,avg_frame_rate,codec_name",
|
||||
"-print_format",
|
||||
"json",
|
||||
str(path_to_video),
|
||||
]
|
||||
config, err, exitcode = capture(command)
|
||||
assert exitcode == 0, f"FFprobe error: {err}"
|
||||
return json.loads(config)["streams"][0]
|
||||
with av.open(str(path_to_video)) as container:
|
||||
stream = container.streams.video[0]
|
||||
ctxt = stream.codec_context
|
||||
rate = stream.average_rate
|
||||
if stream.duration is not None:
|
||||
duration = float(stream.duration * stream.time_base)
|
||||
num_frames = stream.frames
|
||||
else:
|
||||
num_frames = sum(1 for _ in container.decode(video=0))
|
||||
duration = float(num_frames / stream.base_rate)
|
||||
|
||||
return {
|
||||
"width": ctxt.width,
|
||||
"height": ctxt.height,
|
||||
"nb_frames": str(num_frames),
|
||||
"duration": f"{duration:.6f}",
|
||||
"avg_frame_rate": f"{rate.numerator}/{rate.denominator}", # Can be a Fraction
|
||||
"codec_name": stream.codec_context.name,
|
||||
"pix_fmt": stream.codec_context.pix_fmt,
|
||||
}
|
||||
|
||||
|
||||
def get_dir_layout(dirpath: Path) -> Generator[str, None, None]:
|
||||
|
|
|
|||
|
|
@ -198,10 +198,7 @@ def open_file(file_path, in_browser=False):
|
|||
commands = ["cygstart"]
|
||||
file_path = file_path if not in_browser else file_path.parent
|
||||
elif current_os == "Darwin":
|
||||
if is_gif_format():
|
||||
commands = ["ffplay", "-loglevel", config["ffmpeg_loglevel"].lower()]
|
||||
else:
|
||||
commands = ["open"] if not in_browser else ["open", "-R"]
|
||||
commands = ["open"] if not in_browser else ["open", "-R"]
|
||||
else:
|
||||
raise OSError("Unable to identify your operating system...")
|
||||
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ class DummySceneFileWriter(SceneFileWriter):
|
|||
def clean_cache(self):
|
||||
pass
|
||||
|
||||
def write_frame(self, frame_or_renderer):
|
||||
def write_frame(self, frame_or_renderer, num_frames=1):
|
||||
self.i += 1
|
||||
|
||||
|
||||
def _make_scene_file_writer_class(tester: _FramesTester) -> type[SceneFileWriter]:
|
||||
class TestSceneFileWriter(DummySceneFileWriter):
|
||||
def write_frame(self, frame_or_renderer):
|
||||
def write_frame(self, frame_or_renderer, num_frames=1):
|
||||
tester.check_frame(self.i, frame_or_renderer)
|
||||
super().write_frame(frame_or_renderer)
|
||||
super().write_frame(frame_or_renderer, num_frames=num_frames)
|
||||
|
||||
return TestSceneFileWriter
|
||||
|
|
|
|||
2318
poetry.lock
generated
2318
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -28,6 +28,7 @@ packages = [
|
|||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.13"
|
||||
av = ">=9.0.0"
|
||||
click = ">=8.0"
|
||||
cloup = ">=2.0.0"
|
||||
dearpygui = { version = ">=1.0.0", optional = true }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [],
|
||||
"section_index": []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [],
|
||||
"section_index": []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [
|
||||
"SquareToCircle.json",
|
||||
|
|
@ -23,7 +24,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "4.000000",
|
||||
"nb_frames": "60"
|
||||
"nb_frames": "60",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [],
|
||||
"section_index": []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "7.000000",
|
||||
"nb_frames": "105"
|
||||
"nb_frames": "105",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [],
|
||||
"section_index": []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "5.000000",
|
||||
"nb_frames": "75"
|
||||
"nb_frames": "75",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [],
|
||||
"section_index": []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "7.000000",
|
||||
"nb_frames": "105"
|
||||
"nb_frames": "105",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [
|
||||
"SceneWithSections.json",
|
||||
|
|
@ -27,7 +28,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
{
|
||||
"name": "unnamed",
|
||||
|
|
@ -38,7 +40,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "2.000000",
|
||||
"nb_frames": "30"
|
||||
"nb_frames": "30",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
|
|
@ -49,7 +52,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
{
|
||||
"name": "Prepare For Unforeseen Consequences.",
|
||||
|
|
@ -60,7 +64,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "2.000000",
|
||||
"nb_frames": "30"
|
||||
"nb_frames": "30",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
{
|
||||
"name": "unnamed",
|
||||
|
|
@ -71,7 +76,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "6.000000",
|
||||
"nb_frames": "90"
|
||||
"nb_frames": "90",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [
|
||||
"ElaborateSceneWithSections.json",
|
||||
|
|
@ -25,7 +26,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "2.000000",
|
||||
"nb_frames": "30"
|
||||
"nb_frames": "30",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
{
|
||||
"name": "transform to circle",
|
||||
|
|
@ -36,7 +38,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "2.000000",
|
||||
"nb_frames": "30"
|
||||
"nb_frames": "30",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
{
|
||||
"name": "fade out",
|
||||
|
|
@ -47,7 +50,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "2.000000",
|
||||
"nb_frames": "30"
|
||||
"nb_frames": "30",
|
||||
"pix_fmt": "yuv420p"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 1080,
|
||||
"avg_frame_rate": "60/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "60"
|
||||
"nb_frames": "60",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [],
|
||||
"section_index": []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"height": 480,
|
||||
"avg_frame_rate": "15/1",
|
||||
"duration": "1.000000",
|
||||
"nb_frames": "15"
|
||||
"nb_frames": "15",
|
||||
"pix_fmt": "yuv420p"
|
||||
},
|
||||
"section_dir_layout": [],
|
||||
"section_index": []
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import shutil
|
|||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from unittest.mock import patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
|
@ -83,11 +84,20 @@ def test_manim_checkhealth_subcommand():
|
|||
def test_manim_checkhealth_failing_subcommand():
|
||||
command = ["checkhealth"]
|
||||
runner = CliRunner()
|
||||
with tempconfig({"ffmpeg_executable": "/path/to/nowhere"}):
|
||||
true_f = shutil.which
|
||||
|
||||
def mock_f(s):
|
||||
if s == "latex":
|
||||
return None
|
||||
|
||||
return true_f(s)
|
||||
|
||||
with patch.object(shutil, "which", new=mock_f):
|
||||
result = runner.invoke(main, command)
|
||||
|
||||
output_lines = result.output.split("\n")
|
||||
assert "- Checking whether ffmpeg is available ... FAILED" in output_lines
|
||||
assert "- Checking whether ffmpeg is working ... SKIPPED" in output_lines
|
||||
assert "- Checking whether latex is available ... FAILED" in output_lines
|
||||
assert "- Checking whether dvisvgm is available ... SKIPPED" in output_lines
|
||||
|
||||
|
||||
def test_manim_init_subcommand():
|
||||
|
|
|
|||
34
tests/module/animation/test_animation.py
Normal file
34
tests/module/animation/test_animation.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from manim import FadeIn, Scene, config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"run_time",
|
||||
[0, -1],
|
||||
)
|
||||
def test_animation_forbidden_run_time(run_time):
|
||||
test_scene = Scene()
|
||||
with pytest.raises(ValueError, match="Please set the run_time to be positive"):
|
||||
test_scene.play(FadeIn(None, run_time=run_time))
|
||||
|
||||
|
||||
def test_animation_run_time_shorter_than_frame_rate(caplog):
|
||||
test_scene = Scene()
|
||||
test_scene.play(FadeIn(None, run_time=1 / (config.frame_rate + 1)))
|
||||
assert (
|
||||
"Original run time of FadeIn(Mobject) is shorter than current frame rate"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frozen_frame", [False, True])
|
||||
def test_wait_run_time_shorter_than_frame_rate(caplog, frozen_frame):
|
||||
test_scene = Scene()
|
||||
test_scene.wait(1e-9, frozen_frame=frozen_frame)
|
||||
assert (
|
||||
"Original run time of Wait(Mobject) is shorter than current frame rate"
|
||||
in caplog.text
|
||||
)
|
||||
|
|
@ -172,10 +172,10 @@ def test_animationgroup_is_passing_remover_to_nested_animationgroups():
|
|||
|
||||
|
||||
def test_empty_animation_group_fails():
|
||||
with pytest.raises(ValueError, match="Please add at least one Animation"):
|
||||
with pytest.raises(ValueError, match="Please add at least one subanimation."):
|
||||
AnimationGroup().begin()
|
||||
|
||||
|
||||
def test_empty_animation_fails():
|
||||
with pytest.raises(ValueError, match="Please set the run_time to be positive"):
|
||||
FadeIn(None, run_time=0).begin()
|
||||
def test_empty_succession_fails():
|
||||
with pytest.raises(ValueError, match="Please add at least one subanimation."):
|
||||
Succession().begin()
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,8 +1,148 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from manim.utils.commands import capture
|
||||
from manim import DR, Circle, Create, Scene, Star, tempconfig
|
||||
from manim.utils.commands import capture, get_video_metadata
|
||||
|
||||
|
||||
class StarScene(Scene):
|
||||
def construct(self):
|
||||
circle = Circle(fill_opacity=1, color="#ff0000")
|
||||
circle.to_corner(DR).shift(DR)
|
||||
self.add(circle)
|
||||
star = Star()
|
||||
self.play(Create(star))
|
||||
click_path = (
|
||||
Path(__file__).parent.parent.parent
|
||||
/ "docs"
|
||||
/ "source"
|
||||
/ "_static"
|
||||
/ "click.wav"
|
||||
)
|
||||
self.add_sound(click_path)
|
||||
self.wait()
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.parametrize(
|
||||
"transparent",
|
||||
[False, True],
|
||||
)
|
||||
def test_gif_writing(tmp_path, transparent):
|
||||
output_filename = f"gif_{'transparent' if transparent else 'opaque'}"
|
||||
with tempconfig(
|
||||
{
|
||||
"media_dir": tmp_path,
|
||||
"quality": "low_quality",
|
||||
"format": "gif",
|
||||
"transparent": transparent,
|
||||
"output_file": output_filename,
|
||||
}
|
||||
):
|
||||
StarScene().render()
|
||||
|
||||
video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.gif"
|
||||
assert video_path.exists()
|
||||
metadata = get_video_metadata(video_path)
|
||||
# reported duration + avg_frame_rate is slightly off for gifs
|
||||
del metadata["duration"], metadata["avg_frame_rate"]
|
||||
target_metadata = {
|
||||
"width": 854,
|
||||
"height": 480,
|
||||
"nb_frames": "30",
|
||||
"codec_name": "gif",
|
||||
"pix_fmt": "bgra",
|
||||
}
|
||||
assert metadata == target_metadata
|
||||
|
||||
with av.open(video_path) as container:
|
||||
first_frame = next(container.decode(video=0))
|
||||
frame_format = "argb" if transparent else "rgb24"
|
||||
first_frame = first_frame.to_ndarray(format=frame_format)
|
||||
|
||||
target_rgba_corner = (
|
||||
np.array([0, 255, 255, 255], dtype=np.uint8)
|
||||
if transparent
|
||||
else np.array([0, 0, 0], dtype=np.uint8)
|
||||
)
|
||||
np.testing.assert_array_equal(first_frame[0, 0], target_rgba_corner)
|
||||
|
||||
target_rgba_center = (
|
||||
np.array([255, 255, 0, 0]) # components (A, R, G, B)
|
||||
if transparent
|
||||
else np.array([255, 0, 0], dtype=np.uint8)
|
||||
)
|
||||
np.testing.assert_allclose(first_frame[-1, -1], target_rgba_center, atol=5)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.parametrize(
|
||||
"format, transparent, codec, pixel_format",
|
||||
[
|
||||
("mp4", False, "h264", "yuv420p"),
|
||||
("mov", False, "h264", "yuv420p"),
|
||||
("mov", True, "qtrle", "argb"),
|
||||
("webm", False, "vp9", "yuv420p"),
|
||||
("webm", True, "vp9", "yuv420p"),
|
||||
],
|
||||
)
|
||||
def test_codecs(tmp_path, format, transparent, codec, pixel_format):
|
||||
output_filename = f"codec_{format}_{'transparent' if transparent else 'opaque'}"
|
||||
with tempconfig(
|
||||
{
|
||||
"media_dir": tmp_path,
|
||||
"quality": "low_quality",
|
||||
"format": format,
|
||||
"transparent": transparent,
|
||||
"output_file": output_filename,
|
||||
}
|
||||
):
|
||||
StarScene().render()
|
||||
|
||||
video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.{format}"
|
||||
assert video_path.exists()
|
||||
metadata = get_video_metadata(video_path)
|
||||
target_metadata = {
|
||||
"width": 854,
|
||||
"height": 480,
|
||||
"nb_frames": "30",
|
||||
"duration": "2.000000",
|
||||
"avg_frame_rate": "15/1",
|
||||
"codec_name": codec,
|
||||
"pix_fmt": pixel_format,
|
||||
}
|
||||
assert metadata == target_metadata
|
||||
|
||||
with av.open(video_path) as container:
|
||||
if transparent and format == "webm":
|
||||
from av.codec.context import CodecContext
|
||||
|
||||
context = CodecContext.create("libvpx-vp9", "r")
|
||||
packet = next(container.demux(video=0))
|
||||
first_frame = context.decode(packet)[0].to_ndarray(format="argb")
|
||||
else:
|
||||
first_frame = next(container.decode(video=0)).to_ndarray()
|
||||
|
||||
has_samples = [
|
||||
np.any(frame.to_ndarray()) for frame in container.decode(audio=0)
|
||||
]
|
||||
assert any(has_samples), "All audio samples are zero, this is not intended"
|
||||
|
||||
target_rgba_corner = (
|
||||
np.array([0, 0, 0, 0]) if transparent else np.array(16, dtype=np.uint8)
|
||||
)
|
||||
np.testing.assert_array_equal(first_frame[0, 0], target_rgba_corner)
|
||||
|
||||
target_rgba_center = (
|
||||
np.array([255, 255, 0, 0]) # components (A, R, G, B)
|
||||
if transparent
|
||||
else np.array(240, dtype=np.uint8)
|
||||
)
|
||||
np.testing.assert_allclose(first_frame[-1, -1], target_rgba_center, atol=5)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue