From 2419fbcc53ef8d3055a2b4898c0d32072177504c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 10 Jan 2019 17:06:22 -0800 Subject: [PATCH] Changed the way Scene write to videos, where it writes each play or wait call as a separate file, and later concatenates them together. --- manimlib/constants.py | 4 +- manimlib/scene/scene.py | 131 ++++++++++++++------- manimlib/utils/output_directory_getters.py | 37 +++++- stage_scenes.py | 36 ++++-- 4 files changed, 154 insertions(+), 54 deletions(-) diff --git a/manimlib/constants.py b/manimlib/constants.py index 30c115e5..969d5dfa 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -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) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index de24a44b..c4217fe4 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -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) diff --git a/manimlib/utils/output_directory_getters.py b/manimlib/utils/output_directory_getters.py index 113414aa..0bedad08 100644 --- a/manimlib/utils/output_directory_getters.py +++ b/manimlib/utils/output_directory_getters.py @@ -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) diff --git a/stage_scenes.py b/stage_scenes.py index 5babab91..64c6d249 100644 --- a/stage_scenes.py +++ b/stage_scenes.py @@ -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)