Skip to content

Commit

Permalink
Add render option to use x265 with lossless settings
Browse files Browse the repository at this point in the history
By default we use x264 when rendering to the `mp4` format with `crf` set
to 23.

x265 (hevc) has a
[lossless](https://x265.readthedocs.io/en/stable/lossless.html)
mode, where the encoder is configured such that the output is an exact
copy of the input.

Since `manim` scenes consist of text and shapes, the lossless mode works
well for us, and ensures that the output videos will be the highest
quality when desired. This means that users can safely do an editing
pass without risking losing further quality.

Anecdotally, I've noticed slightly better performance than x264 with about
2.5x the file size.

Before:

Before (1,436,872 bytes):

```shell
$ time venv/bin/manim -pqm quad.py Fermat
...
venv/bin/manim -pqm quad.py Fermat  59.41s user 144.46s system 253% cpu 1:20.44 total
```

After (3,494,923 bytes):

```shell
$ time venv/bin/manim -pqm quad.py Fermat --lossless
...
venv/bin/manim -pqm quad.py Fermat --lossless  144.52s user 12.46s system 274% cpu 57.276 total
```

So, I added this as an option (for `mp4` containers).
  • Loading branch information
swenson committed Feb 17, 2025
1 parent 2bdd5ac commit 6fe08b0
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 2 deletions.
12 changes: 12 additions & 0 deletions manim/_config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ class MyScene(Scene): ...
"force_window",
"no_latex_cleanup",
"preview_command",
"lossless",
}

def __init__(self) -> None:
Expand Down Expand Up @@ -591,6 +592,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self:
"enable_wireframe",
"force_window",
"no_latex_cleanup",
"lossless",
]:
setattr(self, key, parser["CLI"].getboolean(key, fallback=False))

Expand Down Expand Up @@ -767,6 +769,7 @@ def digest_args(self, args: argparse.Namespace) -> Self:
"dry_run",
"no_latex_cleanup",
"preview_command",
"lossless",
]:
if hasattr(args, key):
attr = getattr(args, key)
Expand Down Expand Up @@ -1491,6 +1494,15 @@ def zero_pad(self) -> int:
def zero_pad(self, value: int) -> None:
self._set_int_between("zero_pad", value, 0, 9)

@property
def lossless(self) -> bool:
"""Whether to use lossless x265 encoding (mp4 format only)."""
return self._d["lossless"]

@lossless.setter
def lossless(self, value: bool) -> None:
self._set_boolean("lossless", value)

def get_dir(self, key: str, **kwargs: Any) -> Path:
"""Resolve a config option that stores a directory.
Expand Down
6 changes: 6 additions & 0 deletions manim/cli/render/render_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,10 @@ def validate_resolution(
help="Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices.",
default=None,
),
option(
"--lossless",
is_flag=True,
help="Render with lossless x265 encoding (mp4 format only).",
default=False,
),
)
12 changes: 10 additions & 2 deletions manim/scene/scene_file_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,20 @@ def open_partial_movie_stream(self, file_path=None) -> None:

fps = to_av_frame_rate(config.frame_rate)

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)
}
if config.lossless:
partial_movie_file_codec = "libx265"
av_options["x265-params"] = (
"lossless=1" # ffmpeg: set lossless mode for x265
)
else:
partial_movie_file_codec = "libx264"
av_options["crf"] = (
"23" # ffmpeg: -crf, constant rate factor (improved bitrate)
)

if config.movie_file_extension == ".webm":
partial_movie_file_codec = "libvpx-vp9"
Expand Down

0 comments on commit 6fe08b0

Please sign in to comment.