Skip to content

Commit 11ea416

Browse files
smu160pre-commit-ci[bot]irvanalhaq9
authored
Significantly reduce rendering time with a separate thread for writing frames to stream (#3888)
* Add separate thread for writing frames to stream * [pre-commit.ci] pre-commit autoupdate (#3889) updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.4 → v0.5.5](astral-sh/ruff-pre-commit@v0.5.4...v0.5.5) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * Replace the TypeError message code in the _typecheck_input method in … (#3890) * Replace the TypeError message code in the _typecheck_input method in the DrawBorderThenFill class. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * Remove print statements used for debugging * Remove writing process termination - This is probably leftover from back when manim used subprocess to write frames to FFmpeg via stdin * Add type hints to modified methods & instance vars * Fix inline code in docstring & type hint for queue --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Irvanal Haq <[email protected]>
1 parent 17e5a77 commit 11ea416

File tree

1 file changed

+43
-15
lines changed

1 file changed

+43
-15
lines changed

manim/scene/scene_file_writer.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import json
88
import shutil
99
from pathlib import Path
10+
from queue import Queue
11+
from threading import Thread
1012
from typing import TYPE_CHECKING, Any
1113

1214
import av
@@ -16,6 +18,7 @@
1618
from pydub import AudioSegment
1719

1820
from manim import __version__
21+
from manim.typing import PixelArray
1922

2023
from .. import config, logger
2124
from .._config.logger_utils import set_file_logger
@@ -359,6 +362,33 @@ def end_animation(self, allow_write: bool = False):
359362
if write_to_movie() and allow_write:
360363
self.close_partial_movie_stream()
361364

365+
def listen_and_write(self):
366+
"""
367+
For internal use only: blocks until new frame is available on the queue.
368+
"""
369+
while True:
370+
num_frames, frame_data = self.queue.get()
371+
if frame_data is None:
372+
break
373+
374+
self.encode_and_write_frame(frame_data, num_frames)
375+
376+
def encode_and_write_frame(self, frame: PixelArray, num_frames: int) -> None:
377+
"""
378+
For internal use only: takes a given frame in ``np.ndarray`` format and
379+
write it to the stream
380+
"""
381+
for _ in range(num_frames):
382+
# Notes: precomputing reusing packets does not work!
383+
# I.e., you cannot do `packets = encode(...)`
384+
# and reuse it, as it seems that `mux(...)`
385+
# consumes the packet.
386+
# The same issue applies for `av_frame`,
387+
# reusing it renders weird-looking frames.
388+
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
389+
for packet in self.video_stream.encode(av_frame):
390+
self.video_container.mux(packet)
391+
362392
def write_frame(
363393
self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1
364394
):
@@ -379,16 +409,9 @@ def write_frame(
379409
if config.renderer == RendererType.OPENGL
380410
else frame_or_renderer
381411
)
382-
for _ in range(num_frames):
383-
# Notes: precomputing reusing packets does not work!
384-
# I.e., you cannot do `packets = encode(...)`
385-
# and reuse it, as it seems that `mux(...)`
386-
# consumes the packet.
387-
# The same issue applies for `av_frame`,
388-
# reusing it renders weird-looking frames.
389-
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
390-
for packet in self.video_stream.encode(av_frame):
391-
self.video_container.mux(packet)
412+
413+
msg = (num_frames, frame)
414+
self.queue.put(msg)
392415

393416
if is_png_format() and not config["dry_run"]:
394417
image: Image = (
@@ -430,7 +453,7 @@ def save_final_image(self, image: np.ndarray):
430453
image.save(self.image_file_path)
431454
self.print_file_ready_message(self.image_file_path)
432455

433-
def finish(self):
456+
def finish(self) -> None:
434457
"""
435458
Finishes writing to the FFMPEG buffer or writing images
436459
to output directory.
@@ -440,8 +463,6 @@ def finish(self):
440463
frame in the default image directory.
441464
"""
442465
if write_to_movie():
443-
if hasattr(self, "writing_process"):
444-
self.writing_process.terminate()
445466
self.combine_to_movie()
446467
if config.save_sections:
447468
self.combine_to_section_videos()
@@ -455,7 +476,7 @@ def finish(self):
455476
if self.subcaptions:
456477
self.write_subcaption_file()
457478

458-
def open_partial_movie_stream(self, file_path=None):
479+
def open_partial_movie_stream(self, file_path=None) -> None:
459480
"""Open a container holding a video stream.
460481
461482
This is used internally by Manim initialize the container holding
@@ -499,13 +520,20 @@ def open_partial_movie_stream(self, file_path=None):
499520
self.video_container = video_container
500521
self.video_stream = stream
501522

502-
def close_partial_movie_stream(self):
523+
self.queue: Queue[tuple[int, PixelArray | None]] = Queue()
524+
self.writer_thread = Thread(target=self.listen_and_write, args=())
525+
self.writer_thread.start()
526+
527+
def close_partial_movie_stream(self) -> None:
503528
"""Close the currently opened video container.
504529
505530
Used internally by Manim to first flush the remaining packages
506531
in the video stream holding a partial file, and then close
507532
the corresponding container.
508533
"""
534+
self.queue.put((-1, None))
535+
self.writer_thread.join()
536+
509537
for packet in self.video_stream.encode():
510538
self.video_container.mux(packet)
511539

0 commit comments

Comments
 (0)