mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Changed the way Scene write to videos, where it writes each play or wait call as a separate file, and later concatenates them together.
This commit is contained in:
parent
10d64ec74e
commit
2419fbcc53
4 changed files with 154 additions and 54 deletions
|
@ -31,14 +31,12 @@ STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes")
|
|||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
FILE_DIR = os.path.join(THIS_DIR, "files")
|
||||
TEX_DIR = os.path.join(FILE_DIR, "Tex")
|
||||
TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing?
|
||||
# These two may be depricated now.
|
||||
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
|
||||
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
|
||||
|
||||
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR,
|
||||
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR,
|
||||
STAGED_SCENES_DIR]:
|
||||
MOBJECT_DIR, IMAGE_MOBJECT_DIR, STAGED_SCENES_DIR]:
|
||||
if not os.path.exists(folder):
|
||||
os.makedirs(folder)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import inspect
|
|||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
import subprocess
|
||||
import warnings
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
|
@ -24,6 +24,8 @@ from manimlib.utils.iterables import list_update
|
|||
from manimlib.utils.output_directory_getters import add_extension_if_not_present
|
||||
from manimlib.utils.output_directory_getters import get_image_output_directory
|
||||
from manimlib.utils.output_directory_getters import get_movie_output_directory
|
||||
from manimlib.utils.output_directory_getters import get_partial_movie_output_directory
|
||||
from manimlib.utils.output_directory_getters import get_sorted_integer_files
|
||||
|
||||
|
||||
class Scene(Container):
|
||||
|
@ -35,11 +37,9 @@ class Scene(Container):
|
|||
"skip_animations": False,
|
||||
"ignore_waits": False,
|
||||
"write_to_movie": False,
|
||||
"save_frames": False,
|
||||
"save_pngs": False,
|
||||
"pngs_mode": "RGBA",
|
||||
"movie_file_extension": ".mp4",
|
||||
"name": None,
|
||||
"always_continually_update": False,
|
||||
"random_seed": 0,
|
||||
"start_at_animation_number": None,
|
||||
|
@ -57,38 +57,38 @@ class Scene(Container):
|
|||
self.continual_animations = []
|
||||
self.foreground_mobjects = []
|
||||
self.num_plays = 0
|
||||
self.saved_frames = []
|
||||
self.shared_locals = {}
|
||||
self.frame_num = 0
|
||||
self.current_scene_time = 0
|
||||
self.time = 0
|
||||
self.original_skipping_status = self.skip_animations
|
||||
self.stream_lock = False
|
||||
if self.name is None:
|
||||
self.name = self.__class__.__name__
|
||||
if self.random_seed is not None:
|
||||
random.seed(self.random_seed)
|
||||
np.random.seed(self.random_seed)
|
||||
|
||||
self.setup()
|
||||
if self.write_to_movie:
|
||||
self.open_movie_pipe()
|
||||
if self.livestreaming:
|
||||
return None
|
||||
try:
|
||||
self.construct(*self.construct_args)
|
||||
except EndSceneEarlyException:
|
||||
pass
|
||||
|
||||
# Always tack on one last frame, so that scenes
|
||||
# with no play calls still display something
|
||||
self.skip_animations = False
|
||||
self.wait(self.frame_duration)
|
||||
|
||||
self.tear_down()
|
||||
|
||||
if self.write_to_movie:
|
||||
self.close_movie_pipe()
|
||||
print("Played a total of %d animations" % self.num_plays)
|
||||
self.combine_movie_files()
|
||||
|
||||
def handle_play_like_call(func):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
self.handle_animation_skipping()
|
||||
should_write = self.write_to_movie and not self.skip_animations
|
||||
if should_write:
|
||||
self.open_movie_pipe()
|
||||
func(self, *args, **kwargs)
|
||||
self.close_movie_pipe()
|
||||
else:
|
||||
func(self, *args, **kwargs)
|
||||
self.num_plays += 1
|
||||
return wrapper
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
|
@ -109,11 +109,7 @@ class Scene(Container):
|
|||
pass # To be implemented in subclasses
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def set_name(self, name):
|
||||
self.name = name
|
||||
return self
|
||||
return self.__class__.__name__
|
||||
|
||||
def set_variables_as_attrs(self, *objects, **newly_named_objects):
|
||||
"""
|
||||
|
@ -214,6 +210,14 @@ class Scene(Container):
|
|||
|
||||
###
|
||||
|
||||
def get_time(self):
|
||||
return self.time
|
||||
|
||||
def increment_time(self, d_time):
|
||||
self.time += d_time
|
||||
|
||||
###
|
||||
|
||||
def get_top_level_mobjects(self):
|
||||
# Return only those which are not in the family
|
||||
# of another mobject from the scene
|
||||
|
@ -465,13 +469,14 @@ class Scene(Container):
|
|||
self.skip_animations = True
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
@handle_play_like_call
|
||||
def play(self, *args, **kwargs):
|
||||
if self.livestreaming:
|
||||
self.stream_lock = False
|
||||
if len(args) == 0:
|
||||
warnings.warn("Called Scene.play with no animations")
|
||||
return
|
||||
self.handle_animation_skipping()
|
||||
|
||||
animations = self.compile_play_args_to_animation_list(*args)
|
||||
for animation in animations:
|
||||
# This is where kwargs to play like run_time and rate_func
|
||||
|
@ -503,12 +508,10 @@ class Scene(Container):
|
|||
self.continual_update(total_run_time)
|
||||
else:
|
||||
self.continual_update(0)
|
||||
self.num_plays += 1
|
||||
|
||||
if self.livestreaming:
|
||||
self.stream_lock = True
|
||||
thread.start_new_thread(self.idle_stream, ())
|
||||
|
||||
return self
|
||||
|
||||
def idle_stream(self):
|
||||
|
@ -533,6 +536,7 @@ class Scene(Container):
|
|||
return self.mobjects_from_last_animation
|
||||
return []
|
||||
|
||||
@handle_play_like_call
|
||||
def wait(self, duration=DEFAULT_WAIT_TIME):
|
||||
if self.should_continually_update():
|
||||
total_time = 0
|
||||
|
@ -554,7 +558,7 @@ class Scene(Container):
|
|||
def wait_to(self, time, assert_positive=True):
|
||||
if self.ignore_waits:
|
||||
return
|
||||
time -= self.current_scene_time
|
||||
time -= self.get_time()
|
||||
if assert_positive:
|
||||
assert(time >= 0)
|
||||
elif time < 0:
|
||||
|
@ -575,16 +579,15 @@ class Scene(Container):
|
|||
def add_frames(self, *frames):
|
||||
if self.skip_animations:
|
||||
return
|
||||
self.current_scene_time += len(frames) * self.frame_duration
|
||||
self.increment_time(len(frames) * self.frame_duration)
|
||||
if self.write_to_movie:
|
||||
for frame in frames:
|
||||
if self.save_pngs:
|
||||
self.save_image(
|
||||
"frame" + str(self.frame_num), self.pngs_mode, True)
|
||||
"frame" + str(self.frame_num), self.pngs_mode, True
|
||||
)
|
||||
self.frame_num = self.frame_num + 1
|
||||
self.writing_process.stdin.write(frame.tostring())
|
||||
if self.save_frames:
|
||||
self.saved_frames += list(frames)
|
||||
|
||||
# Display methods
|
||||
|
||||
|
@ -615,17 +618,26 @@ class Scene(Container):
|
|||
if extension is None:
|
||||
extension = self.movie_file_extension
|
||||
if name is None:
|
||||
name = self.name
|
||||
name = str(self)
|
||||
file_path = os.path.join(directory, name)
|
||||
if not file_path.endswith(extension):
|
||||
file_path += extension
|
||||
return file_path
|
||||
|
||||
def get_partial_movie_directory(self):
|
||||
return get_partial_movie_output_directory(
|
||||
self.__class__, self.camera_config, self.frame_duration
|
||||
)
|
||||
|
||||
def open_movie_pipe(self):
|
||||
name = str(self)
|
||||
file_path = self.get_movie_file_path(name)
|
||||
temp_file_path = file_path.replace(name, name + "Temp")
|
||||
print("Writing to %s" % temp_file_path)
|
||||
directory = self.get_partial_movie_directory()
|
||||
file_path = os.path.join(
|
||||
directory, "{}{}".format(
|
||||
self.num_plays,
|
||||
self.movie_file_extension,
|
||||
)
|
||||
)
|
||||
temp_file_path = file_path.replace(".", "_temp.")
|
||||
self.args_to_rename_file = (temp_file_path, file_path)
|
||||
|
||||
fps = int(1 / self.frame_duration)
|
||||
|
@ -664,18 +676,55 @@ class Scene(Container):
|
|||
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
|
||||
else:
|
||||
command += [temp_file_path]
|
||||
# self.writing_process = sp.Popen(command, stdin=sp.PIPE, shell=True)
|
||||
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
|
||||
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
||||
|
||||
def close_movie_pipe(self):
|
||||
self.writing_process.stdin.close()
|
||||
self.writing_process.wait()
|
||||
if self.livestreaming:
|
||||
return True
|
||||
if os.name == 'nt':
|
||||
shutil.move(*self.args_to_rename_file)
|
||||
shutil.move(*self.args_to_rename_file)
|
||||
|
||||
def combine_movie_files(self):
|
||||
partial_movie_file_directory = self.get_partial_movie_directory()
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True
|
||||
}
|
||||
if self.start_at_animation_number is not None:
|
||||
kwargs["min_index"] = self.start_at_animation_number
|
||||
if self.end_at_animation_number is not None:
|
||||
kwargs["max_index"] = self.end_at_animation_number
|
||||
else:
|
||||
os.rename(*self.args_to_rename_file)
|
||||
kwargs["remove_indices_greater_than"] = self.num_plays - 1
|
||||
partial_movie_files = get_sorted_integer_files(
|
||||
partial_movie_file_directory,
|
||||
**kwargs
|
||||
)
|
||||
# Write a file partial_file_list.txt containing all
|
||||
# partial movie files
|
||||
file_list = os.path.join(
|
||||
partial_movie_file_directory,
|
||||
"partial_movie_file_list.txt"
|
||||
)
|
||||
with open(file_list, 'w') as fp:
|
||||
for pf_path in partial_movie_files:
|
||||
fp.write("file {}\n".format(pf_path))
|
||||
|
||||
movie_file_path = self.get_movie_file_path()
|
||||
commands = [
|
||||
FFMPEG_BIN,
|
||||
'-y', # overwrite output file if it exists
|
||||
'-f', 'concat',
|
||||
'-safe', '0',
|
||||
'-i', file_list,
|
||||
'-c', 'copy',
|
||||
'-an', # Tells FFMPEG not to expect any audio
|
||||
'-loglevel', 'error',
|
||||
movie_file_path
|
||||
]
|
||||
subprocess.call(commands)
|
||||
os.remove(file_list)
|
||||
print("File ready at {}".format(movie_file_path))
|
||||
|
||||
def tex(self, latex):
|
||||
eq = TextMobject(latex)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import inspect
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import THIS_DIR
|
||||
from manimlib.constants import VIDEO_DIR
|
||||
|
||||
|
||||
|
@ -35,6 +34,40 @@ def get_movie_output_directory(scene_class, camera_config, frame_duration):
|
|||
return guarantee_existance(os.path.join(directory, sub_dir))
|
||||
|
||||
|
||||
def get_partial_movie_output_directory(scene_class, camera_config, frame_duration):
|
||||
directory = get_movie_output_directory(scene_class, camera_config, frame_duration)
|
||||
return guarantee_existance(
|
||||
os.path.join(directory, scene_class.__name__)
|
||||
)
|
||||
|
||||
|
||||
def get_image_output_directory(scene_class, sub_dir="images"):
|
||||
directory = get_scene_output_directory(scene_class)
|
||||
return guarantee_existance(os.path.join(directory, sub_dir))
|
||||
|
||||
|
||||
def get_sorted_integer_files(directory,
|
||||
min_index=0,
|
||||
max_index=np.inf,
|
||||
remove_non_integer_files=False,
|
||||
remove_indices_greater_than=None):
|
||||
indexed_files = []
|
||||
for file in os.listdir(directory):
|
||||
if '.' in file:
|
||||
index_str = file[:file.index('.')]
|
||||
else:
|
||||
index_str = file
|
||||
|
||||
full_path = os.path.join(directory, file)
|
||||
if index_str.isdigit():
|
||||
index = int(index_str)
|
||||
if remove_indices_greater_than is not None:
|
||||
if index > remove_indices_greater_than:
|
||||
os.remove(full_path)
|
||||
continue
|
||||
if index >= min_index and index < max_index:
|
||||
indexed_files.append((index, file))
|
||||
elif remove_non_integer_files:
|
||||
os.remove(full_path)
|
||||
indexed_files.sort(key=lambda p: p[0])
|
||||
return map(lambda p: p[1], indexed_files)
|
||||
|
|
|
@ -9,6 +9,8 @@ from manimlib.constants import PRODUCTION_QUALITY_FRAME_DURATION
|
|||
from manimlib.config import get_module
|
||||
from manimlib.extract_scene import is_child_scene
|
||||
from manimlib.utils.output_directory_getters import get_movie_output_directory
|
||||
from manimlib.utils.output_directory_getters import get_partial_movie_output_directory
|
||||
from manimlib.utils.output_directory_getters import get_sorted_integer_files
|
||||
|
||||
|
||||
def get_sorted_scene_classes(module_name):
|
||||
|
@ -35,23 +37,35 @@ def stage_animations(module_name):
|
|||
if len(scene_classes) == 0:
|
||||
print("There are no rendered animations from this module")
|
||||
return
|
||||
output_directory_kwargs = {
|
||||
"camera_config": PRODUCTION_QUALITY_CAMERA_CONFIG,
|
||||
"frame_duration": PRODUCTION_QUALITY_FRAME_DURATION,
|
||||
}
|
||||
animation_dir = get_movie_output_directory(
|
||||
scene_classes[0],
|
||||
PRODUCTION_QUALITY_CAMERA_CONFIG,
|
||||
PRODUCTION_QUALITY_FRAME_DURATION,
|
||||
scene_classes[0], **output_directory_kwargs
|
||||
)
|
||||
files = os.listdir(animation_dir)
|
||||
sorted_files = []
|
||||
for scene_class in scene_classes:
|
||||
scene_name = scene_class.__name__
|
||||
for clip in [f for f in files if f.startswith(scene_name + ".")]:
|
||||
sorted_files.append(os.path.join(animation_dir, clip))
|
||||
# Partial movie file directory
|
||||
pmf_dir = get_partial_movie_output_directory(
|
||||
scene_class, **output_directory_kwargs
|
||||
)
|
||||
if os.path.exists(pmf_dir):
|
||||
for file in get_sorted_integer_files(pmf_dir):
|
||||
sorted_files.append(
|
||||
os.path.join(pmf_dir, file)
|
||||
)
|
||||
else:
|
||||
for clip in [f for f in files if f.startswith(scene_name + ".")]:
|
||||
sorted_files.append(os.path.join(animation_dir, clip))
|
||||
|
||||
animation_subdir = os.path.dirname(animation_dir)
|
||||
count = 0
|
||||
while True:
|
||||
staged_scenes_dir = os.path.join(
|
||||
animation_subdir, "staged_scenes_%d" % count
|
||||
animation_subdir, "staged_scenes_{}".format(count)
|
||||
)
|
||||
if not os.path.exists(staged_scenes_dir):
|
||||
os.makedirs(staged_scenes_dir)
|
||||
|
@ -59,10 +73,16 @@ def stage_animations(module_name):
|
|||
# Otherwise, keep trying new names until
|
||||
# there is a free one
|
||||
count += 1
|
||||
for count, f in enumerate(sorted_files):
|
||||
for count, f in reversed(list(enumerate(sorted_files))):
|
||||
# Going in reversed order means that when finder
|
||||
# sorts by date modified, it shows up in the
|
||||
# correct order
|
||||
symlink_name = os.path.join(
|
||||
staged_scenes_dir,
|
||||
"Scene_%03d" % count + f.split(os.sep)[-1]
|
||||
"Scene_{:03}_{}".format(
|
||||
count,
|
||||
"".join(f.split(os.sep)[-2:])
|
||||
)
|
||||
)
|
||||
os.symlink(f, symlink_name)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue