Skip to content

Commit 6fe08b0

Browse files
committed
Add render option to use x265 with lossless settings
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).
1 parent 2bdd5ac commit 6fe08b0

File tree

3 files changed

+28
-2
lines changed

3 files changed

+28
-2
lines changed

manim/_config/utils.py

+12
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ class MyScene(Scene): ...
320320
"force_window",
321321
"no_latex_cleanup",
322322
"preview_command",
323+
"lossless",
323324
}
324325

325326
def __init__(self) -> None:
@@ -591,6 +592,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self:
591592
"enable_wireframe",
592593
"force_window",
593594
"no_latex_cleanup",
595+
"lossless",
594596
]:
595597
setattr(self, key, parser["CLI"].getboolean(key, fallback=False))
596598

@@ -767,6 +769,7 @@ def digest_args(self, args: argparse.Namespace) -> Self:
767769
"dry_run",
768770
"no_latex_cleanup",
769771
"preview_command",
772+
"lossless",
770773
]:
771774
if hasattr(args, key):
772775
attr = getattr(args, key)
@@ -1491,6 +1494,15 @@ def zero_pad(self) -> int:
14911494
def zero_pad(self, value: int) -> None:
14921495
self._set_int_between("zero_pad", value, 0, 9)
14931496

1497+
@property
1498+
def lossless(self) -> bool:
1499+
"""Whether to use lossless x265 encoding (mp4 format only)."""
1500+
return self._d["lossless"]
1501+
1502+
@lossless.setter
1503+
def lossless(self, value: bool) -> None:
1504+
self._set_boolean("lossless", value)
1505+
14941506
def get_dir(self, key: str, **kwargs: Any) -> Path:
14951507
"""Resolve a config option that stores a directory.
14961508

manim/cli/render/render_options.py

+6
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,10 @@ def validate_resolution(
212212
help="Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices.",
213213
default=None,
214214
),
215+
option(
216+
"--lossless",
217+
is_flag=True,
218+
help="Render with lossless x265 encoding (mp4 format only).",
219+
default=False,
220+
),
215221
)

manim/scene/scene_file_writer.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -536,12 +536,20 @@ def open_partial_movie_stream(self, file_path=None) -> None:
536536

537537
fps = to_av_frame_rate(config.frame_rate)
538538

539-
partial_movie_file_codec = "libx264"
540539
partial_movie_file_pix_fmt = "yuv420p"
541540
av_options = {
542541
"an": "1", # ffmpeg: -an, no audio
543-
"crf": "23", # ffmpeg: -crf, constant rate factor (improved bitrate)
544542
}
543+
if config.lossless:
544+
partial_movie_file_codec = "libx265"
545+
av_options["x265-params"] = (
546+
"lossless=1" # ffmpeg: set lossless mode for x265
547+
)
548+
else:
549+
partial_movie_file_codec = "libx264"
550+
av_options["crf"] = (
551+
"23" # ffmpeg: -crf, constant rate factor (improved bitrate)
552+
)
545553

546554
if config.movie_file_extension == ".webm":
547555
partial_movie_file_codec = "libvpx-vp9"

0 commit comments

Comments
 (0)