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:
Grant Sanderson 2019-01-10 17:06:22 -08:00
parent 10d64ec74e
commit 2419fbcc53
4 changed files with 154 additions and 54 deletions

View file

@ -31,14 +31,12 @@ STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes")
THIS_DIR = os.path.dirname(os.path.realpath(__file__)) THIS_DIR = os.path.dirname(os.path.realpath(__file__))
FILE_DIR = os.path.join(THIS_DIR, "files") FILE_DIR = os.path.join(THIS_DIR, "files")
TEX_DIR = os.path.join(FILE_DIR, "Tex") TEX_DIR = os.path.join(FILE_DIR, "Tex")
TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing?
# These two may be depricated now. # These two may be depricated now.
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects") MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image") IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR, for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR,
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR, STAGED_SCENES_DIR]:
STAGED_SCENES_DIR]:
if not os.path.exists(folder): if not os.path.exists(folder):
os.makedirs(folder) os.makedirs(folder)

View file

@ -5,7 +5,7 @@ import inspect
import os import os
import random import random
import shutil import shutil
import subprocess as sp import subprocess
import warnings import warnings
from tqdm import tqdm as ProgressDisplay 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 add_extension_if_not_present
from manimlib.utils.output_directory_getters import get_image_output_directory 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_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): class Scene(Container):
@ -35,11 +37,9 @@ class Scene(Container):
"skip_animations": False, "skip_animations": False,
"ignore_waits": False, "ignore_waits": False,
"write_to_movie": False, "write_to_movie": False,
"save_frames": False,
"save_pngs": False, "save_pngs": False,
"pngs_mode": "RGBA", "pngs_mode": "RGBA",
"movie_file_extension": ".mp4", "movie_file_extension": ".mp4",
"name": None,
"always_continually_update": False, "always_continually_update": False,
"random_seed": 0, "random_seed": 0,
"start_at_animation_number": None, "start_at_animation_number": None,
@ -57,38 +57,38 @@ class Scene(Container):
self.continual_animations = [] self.continual_animations = []
self.foreground_mobjects = [] self.foreground_mobjects = []
self.num_plays = 0 self.num_plays = 0
self.saved_frames = []
self.shared_locals = {}
self.frame_num = 0 self.frame_num = 0
self.current_scene_time = 0 self.time = 0
self.original_skipping_status = self.skip_animations self.original_skipping_status = self.skip_animations
self.stream_lock = False self.stream_lock = False
if self.name is None:
self.name = self.__class__.__name__
if self.random_seed is not None: if self.random_seed is not None:
random.seed(self.random_seed) random.seed(self.random_seed)
np.random.seed(self.random_seed) np.random.seed(self.random_seed)
self.setup() self.setup()
if self.write_to_movie:
self.open_movie_pipe()
if self.livestreaming: if self.livestreaming:
return None return None
try: try:
self.construct(*self.construct_args) self.construct(*self.construct_args)
except EndSceneEarlyException: except EndSceneEarlyException:
pass 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() self.tear_down()
if self.write_to_movie: if self.write_to_movie:
self.close_movie_pipe() self.combine_movie_files()
print("Played a total of %d animations" % self.num_plays)
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): def setup(self):
""" """
@ -109,11 +109,7 @@ class Scene(Container):
pass # To be implemented in subclasses pass # To be implemented in subclasses
def __str__(self): def __str__(self):
return self.name return self.__class__.__name__
def set_name(self, name):
self.name = name
return self
def set_variables_as_attrs(self, *objects, **newly_named_objects): 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): def get_top_level_mobjects(self):
# Return only those which are not in the family # Return only those which are not in the family
# of another mobject from the scene # of another mobject from the scene
@ -465,13 +469,14 @@ class Scene(Container):
self.skip_animations = True self.skip_animations = True
raise EndSceneEarlyException() raise EndSceneEarlyException()
@handle_play_like_call
def play(self, *args, **kwargs): def play(self, *args, **kwargs):
if self.livestreaming: if self.livestreaming:
self.stream_lock = False self.stream_lock = False
if len(args) == 0: if len(args) == 0:
warnings.warn("Called Scene.play with no animations") warnings.warn("Called Scene.play with no animations")
return return
self.handle_animation_skipping()
animations = self.compile_play_args_to_animation_list(*args) animations = self.compile_play_args_to_animation_list(*args)
for animation in animations: for animation in animations:
# This is where kwargs to play like run_time and rate_func # 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) self.continual_update(total_run_time)
else: else:
self.continual_update(0) self.continual_update(0)
self.num_plays += 1
if self.livestreaming: if self.livestreaming:
self.stream_lock = True self.stream_lock = True
thread.start_new_thread(self.idle_stream, ()) thread.start_new_thread(self.idle_stream, ())
return self return self
def idle_stream(self): def idle_stream(self):
@ -533,6 +536,7 @@ class Scene(Container):
return self.mobjects_from_last_animation return self.mobjects_from_last_animation
return [] return []
@handle_play_like_call
def wait(self, duration=DEFAULT_WAIT_TIME): def wait(self, duration=DEFAULT_WAIT_TIME):
if self.should_continually_update(): if self.should_continually_update():
total_time = 0 total_time = 0
@ -554,7 +558,7 @@ class Scene(Container):
def wait_to(self, time, assert_positive=True): def wait_to(self, time, assert_positive=True):
if self.ignore_waits: if self.ignore_waits:
return return
time -= self.current_scene_time time -= self.get_time()
if assert_positive: if assert_positive:
assert(time >= 0) assert(time >= 0)
elif time < 0: elif time < 0:
@ -575,16 +579,15 @@ class Scene(Container):
def add_frames(self, *frames): def add_frames(self, *frames):
if self.skip_animations: if self.skip_animations:
return return
self.current_scene_time += len(frames) * self.frame_duration self.increment_time(len(frames) * self.frame_duration)
if self.write_to_movie: if self.write_to_movie:
for frame in frames: for frame in frames:
if self.save_pngs: if self.save_pngs:
self.save_image( 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.frame_num = self.frame_num + 1
self.writing_process.stdin.write(frame.tostring()) self.writing_process.stdin.write(frame.tostring())
if self.save_frames:
self.saved_frames += list(frames)
# Display methods # Display methods
@ -615,17 +618,26 @@ class Scene(Container):
if extension is None: if extension is None:
extension = self.movie_file_extension extension = self.movie_file_extension
if name is None: if name is None:
name = self.name name = str(self)
file_path = os.path.join(directory, name) file_path = os.path.join(directory, name)
if not file_path.endswith(extension): if not file_path.endswith(extension):
file_path += extension file_path += extension
return file_path 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): def open_movie_pipe(self):
name = str(self) directory = self.get_partial_movie_directory()
file_path = self.get_movie_file_path(name) file_path = os.path.join(
temp_file_path = file_path.replace(name, name + "Temp") directory, "{}{}".format(
print("Writing to %s" % temp_file_path) self.num_plays,
self.movie_file_extension,
)
)
temp_file_path = file_path.replace(".", "_temp.")
self.args_to_rename_file = (temp_file_path, file_path) self.args_to_rename_file = (temp_file_path, file_path)
fps = int(1 / self.frame_duration) fps = int(1 / self.frame_duration)
@ -664,18 +676,55 @@ class Scene(Container):
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT] command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
else: else:
command += [temp_file_path] command += [temp_file_path]
# self.writing_process = sp.Popen(command, stdin=sp.PIPE, shell=True) self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
def close_movie_pipe(self): def close_movie_pipe(self):
self.writing_process.stdin.close() self.writing_process.stdin.close()
self.writing_process.wait() self.writing_process.wait()
if self.livestreaming: if self.livestreaming:
return True 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: 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): def tex(self, latex):
eq = TextMobject(latex) eq = TextMobject(latex)

View file

@ -1,7 +1,6 @@
import inspect
import os import os
import numpy as np
from manimlib.constants import THIS_DIR
from manimlib.constants import VIDEO_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)) 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"): def get_image_output_directory(scene_class, sub_dir="images"):
directory = get_scene_output_directory(scene_class) directory = get_scene_output_directory(scene_class)
return guarantee_existance(os.path.join(directory, sub_dir)) 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)

View file

@ -9,6 +9,8 @@ from manimlib.constants import PRODUCTION_QUALITY_FRAME_DURATION
from manimlib.config import get_module from manimlib.config import get_module
from manimlib.extract_scene import is_child_scene 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_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): def get_sorted_scene_classes(module_name):
@ -35,23 +37,35 @@ def stage_animations(module_name):
if len(scene_classes) == 0: if len(scene_classes) == 0:
print("There are no rendered animations from this module") print("There are no rendered animations from this module")
return return
output_directory_kwargs = {
"camera_config": PRODUCTION_QUALITY_CAMERA_CONFIG,
"frame_duration": PRODUCTION_QUALITY_FRAME_DURATION,
}
animation_dir = get_movie_output_directory( animation_dir = get_movie_output_directory(
scene_classes[0], scene_classes[0], **output_directory_kwargs
PRODUCTION_QUALITY_CAMERA_CONFIG,
PRODUCTION_QUALITY_FRAME_DURATION,
) )
files = os.listdir(animation_dir) files = os.listdir(animation_dir)
sorted_files = [] sorted_files = []
for scene_class in scene_classes: for scene_class in scene_classes:
scene_name = scene_class.__name__ scene_name = scene_class.__name__
for clip in [f for f in files if f.startswith(scene_name + ".")]: # Partial movie file directory
sorted_files.append(os.path.join(animation_dir, clip)) 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) animation_subdir = os.path.dirname(animation_dir)
count = 0 count = 0
while True: while True:
staged_scenes_dir = os.path.join( 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): if not os.path.exists(staged_scenes_dir):
os.makedirs(staged_scenes_dir) os.makedirs(staged_scenes_dir)
@ -59,10 +73,16 @@ def stage_animations(module_name):
# Otherwise, keep trying new names until # Otherwise, keep trying new names until
# there is a free one # there is a free one
count += 1 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( symlink_name = os.path.join(
staged_scenes_dir, 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) os.symlink(f, symlink_name)