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__))
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)

View file

@ -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)

View file

@ -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)

View file

@ -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)