Skip to content

Commit 735de62

Browse files
smu160pre-commit-ci[bot]irvanalhaq9
authored andcommitted
Significantly reduce rendering time with a separate thread for writing frames to stream (ManimCommunity#3888)
* Add separate thread for writing frames to stream * [pre-commit.ci] pre-commit autoupdate (ManimCommunity#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 … (ManimCommunity#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 7e8c5c8 commit 735de62

File tree

1 file changed

+55
-13
lines changed

1 file changed

+55
-13
lines changed

manim/file_writer/file_writer.py

Lines changed: 55 additions & 13 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
@@ -20,7 +22,13 @@
2022
from manim._config.logger_utils import set_file_logger
2123
from manim.file_writer.protocols import FileWriterProtocol
2224
from manim.file_writer.sections import DefaultSectionType, Section
23-
from manim.utils.file_ops import (
25+
26+
from manim.typing import PixelArray
27+
28+
from .. import config, logger
29+
from .._config.logger_utils import set_file_logger
30+
from ..constants import RendererType
31+
from ..utils.file_ops import (
2432
add_extension_if_not_present,
2533
add_version_before_extension,
2634
guarantee_existence,
@@ -361,7 +369,36 @@ def end_animation(self, allow_write: bool = False) -> None:
361369
self.close_partial_movie_stream()
362370
self.num_plays += 1
363371

364-
def write_frame(self, frame: PixelArray, num_frames: int = 1) -> None:
372+
def listen_and_write(self):
373+
"""
374+
For internal use only: blocks until new frame is available on the queue.
375+
"""
376+
while True:
377+
num_frames, frame_data = self.queue.get()
378+
if frame_data is None:
379+
break
380+
381+
self.encode_and_write_frame(frame_data, num_frames)
382+
383+
def encode_and_write_frame(self, frame: PixelArray, num_frames: int) -> None:
384+
"""
385+
For internal use only: takes a given frame in ``np.ndarray`` format and
386+
write it to the stream
387+
"""
388+
for _ in range(num_frames):
389+
# Notes: precomputing reusing packets does not work!
390+
# I.e., you cannot do `packets = encode(...)`
391+
# and reuse it, as it seems that `mux(...)`
392+
# consumes the packet.
393+
# The same issue applies for `av_frame`,
394+
# reusing it renders weird-looking frames.
395+
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
396+
for packet in self.video_stream.encode(av_frame):
397+
self.video_container.mux(packet)
398+
399+
def write_frame(
400+
self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1
401+
):
365402
"""
366403
Used internally by Manim to write a frame to
367404
the FFMPEG input buffer.
@@ -374,16 +411,14 @@ def write_frame(self, frame: PixelArray, num_frames: int = 1) -> None:
374411
The number of times to write frame.
375412
"""
376413
if write_to_movie():
377-
for _ in range(num_frames):
378-
# Notes: precomputing reusing packets does not work!
379-
# I.e., you cannot do `packets = encode(...)`
380-
# and reuse it, as it seems that `mux(...)`
381-
# consumes the packet.
382-
# The same issue applies for `av_frame`,
383-
# reusing it renders weird-looking frames.
384-
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
385-
for packet in self.video_stream.encode(av_frame):
386-
self.video_container.mux(packet)
414+
frame: np.ndarray = (
415+
frame_or_renderer.get_frame()
416+
if config.renderer == RendererType.OPENGL
417+
else frame_or_renderer
418+
)
419+
420+
msg = (num_frames, frame)
421+
self.queue.put(msg)
387422

388423
if is_png_format() and not config.dry_run:
389424
image = Image.fromarray(frame)
@@ -444,7 +479,7 @@ def finish(self) -> None:
444479
if self.subcaptions:
445480
self.write_subcaption_file()
446481

447-
def open_partial_movie_stream(self, file_path: str | None = None) -> None:
482+
def open_partial_movie_stream(self, file_path=None) -> None:
448483
"""Open a container holding a video stream.
449484
450485
This is used internally by Manim initialize the container holding
@@ -488,13 +523,20 @@ def open_partial_movie_stream(self, file_path: str | None = None) -> None:
488523
self.video_container = video_container
489524
self.video_stream = stream
490525

526+
self.queue: Queue[tuple[int, PixelArray | None]] = Queue()
527+
self.writer_thread = Thread(target=self.listen_and_write, args=())
528+
self.writer_thread.start()
529+
491530
def close_partial_movie_stream(self) -> None:
492531
"""Close the currently opened video container.
493532
494533
Used internally by Manim to first flush the remaining packages
495534
in the video stream holding a partial file, and then close
496535
the corresponding container.
497536
"""
537+
self.queue.put((-1, None))
538+
self.writer_thread.join()
539+
498540
for packet in self.video_stream.encode():
499541
self.video_container.mux(packet)
500542

0 commit comments

Comments
 (0)