mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
First pass at SceneFileWriter refactor
This commit is contained in:
parent
77f0ca1ad7
commit
8ae0556394
18 changed files with 432 additions and 384 deletions
8
active_projects/clacks/all_questions_scenes.py
Normal file
8
active_projects/clacks/all_questions_scenes.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from active_projects import clacks
|
||||
|
||||
output_directory = "clacks_question"
|
||||
all_scenes = [
|
||||
clacks.NameIntro,
|
||||
clacks.MathAndPhysicsConspiring,
|
||||
clacks.LightBouncing,
|
||||
]
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
from big_ol_pile_of_manim_imports import *
|
||||
|
||||
from active_projects.clacks import BlocksAndWallExample
|
||||
from active_projects.clacks.question import BlocksAndWallExample
|
||||
|
||||
|
||||
class NameBump(BlocksAndWallExample):
|
||||
|
@ -72,7 +72,6 @@ class NameBump(BlocksAndWallExample):
|
|||
block.label.set_fill(YELLOW, opacity=1)
|
||||
|
||||
|
||||
|
||||
# for name in names:
|
||||
# file_name = name.replace(".", "")
|
||||
# file_name += " Name Bump"
|
|
@ -346,10 +346,7 @@ class BlocksAndWallScene(Scene):
|
|||
self.counter_mob.set_value(n_clacks)
|
||||
|
||||
def create_sound_file(self, clack_data):
|
||||
directory = get_scene_output_directory(BlocksAndWallScene)
|
||||
clack_file = os.path.join(
|
||||
directory, 'sounds', self.collision_sound,
|
||||
)
|
||||
clack_file = os.path.join(SOUND_DIR, self.collision_sound)
|
||||
output_file = self.get_movie_file_path(extension='.wav')
|
||||
times = [
|
||||
time
|
BIN
active_projects/clacks/question/480p15/NameIntro.mp4
Normal file
BIN
active_projects/clacks/question/480p15/NameIntro.mp4
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,4 @@
|
|||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00000.mp4'
|
||||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00001.mp4'
|
||||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00002.mp4'
|
||||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00003.mp4'
|
|
@ -1,5 +1,5 @@
|
|||
from big_ol_pile_of_manim_imports import *
|
||||
from active_projects.clacks import *
|
||||
from active_projects.clacks.question import *
|
||||
from old_projects.div_curl import ShowTwoPopulations
|
||||
|
||||
|
||||
|
@ -206,10 +206,7 @@ class AskAboutFindingNewVelocities(Scene):
|
|||
self.show_value_on_equations()
|
||||
|
||||
def add_clack_sound_file(self):
|
||||
self.clack_file = os.path.join(
|
||||
VIDEO_DIR, "active_projects",
|
||||
"clacks", "sounds", "clack.wav"
|
||||
)
|
||||
self.clack_file = os.path.join(SOUND_DIR, "clack.wav")
|
||||
|
||||
def add_floor(self):
|
||||
floor = self.floor = Line(
|
|
@ -88,7 +88,7 @@ from manimlib.utils.color import *
|
|||
from manimlib.utils.config_ops import *
|
||||
from manimlib.utils.images import *
|
||||
from manimlib.utils.iterables import *
|
||||
from manimlib.utils.output_directory_getters import *
|
||||
from manimlib.utils.file_ops import *
|
||||
from manimlib.utils.paths import *
|
||||
from manimlib.utils.rate_functions import *
|
||||
from manimlib.utils.simple_functions import *
|
||||
|
|
|
@ -33,7 +33,7 @@ def parse_cli():
|
|||
help="Render the scene as a movie file",
|
||||
),
|
||||
parser.add_argument(
|
||||
"-s", "--show_last_frame",
|
||||
"-s", "--save_last_frame",
|
||||
action="store_true",
|
||||
help="Save the last frame and open the image file",
|
||||
),
|
||||
|
@ -73,7 +73,7 @@ def parse_cli():
|
|||
help="Write all the scenes from a file",
|
||||
),
|
||||
parser.add_argument(
|
||||
"-o", "--output_file_name",
|
||||
"-o", "--file_name",
|
||||
help="Specify the name of the output file, if"
|
||||
"it should be different from the scene class name",
|
||||
)
|
||||
|
@ -155,36 +155,25 @@ def get_module(file_name):
|
|||
|
||||
|
||||
def get_configuration(args):
|
||||
if args.output_file_name is not None:
|
||||
output_file_name_root, output_file_name_ext = os.path.splitext(
|
||||
args.output_file_name)
|
||||
expected_ext = '.png' if args.show_last_frame else '.mp4'
|
||||
if output_file_name_ext not in ['', expected_ext]:
|
||||
print("WARNING: The output will be to (doubly-dotted) %s%s" %
|
||||
output_file_name_root % expected_ext)
|
||||
output_file_name = args.output_file_name
|
||||
else:
|
||||
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
|
||||
output_file_name = output_file_name_root
|
||||
else:
|
||||
output_file_name = args.output_file_name
|
||||
|
||||
file_writer_config = {
|
||||
# By default, write to file
|
||||
"write_to_movie": args.write_to_movie or not args.save_last_frame,
|
||||
"save_last_frame": args.save_last_frame,
|
||||
"save_pngs": args.save_pngs,
|
||||
# If -t is passed in (for transparent), this will be RGBA
|
||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||
"movie_file_extension": ".mov" if args.transparent else ".mp4",
|
||||
"file_name": args.file_name,
|
||||
}
|
||||
config = {
|
||||
"module": get_module(args.file),
|
||||
"scene_names": args.scene_names,
|
||||
"open_video_upon_completion": args.preview,
|
||||
"show_file_in_finder": args.show_file_in_finder,
|
||||
# By default, write to file
|
||||
"write_to_movie": args.write_to_movie or not args.show_last_frame,
|
||||
"show_last_frame": args.show_last_frame,
|
||||
"save_pngs": args.save_pngs,
|
||||
# If -t is passed in (for transparent), this will be RGBA
|
||||
"saved_image_mode": "RGBA" if args.transparent else "RGB",
|
||||
"movie_file_extension": ".mov" if args.transparent else ".mp4",
|
||||
"file_writer_config": file_writer_config,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"ignore_waits": args.preview,
|
||||
"write_all": args.write_all,
|
||||
"output_file_name": output_file_name,
|
||||
"start_at_animation_number": args.start_at_animation_number,
|
||||
"end_at_animation_number": None,
|
||||
"sound": args.sound,
|
||||
|
@ -241,7 +230,7 @@ def get_configuration(args):
|
|||
config["start_at_animation_number"] = int(stan)
|
||||
|
||||
config["skip_animations"] = any([
|
||||
config["show_last_frame"] and not config["write_to_movie"],
|
||||
file_writer_config["save_last_frame"],
|
||||
config["start_at_animation_number"],
|
||||
])
|
||||
return config
|
||||
|
|
|
@ -25,6 +25,7 @@ with open("media_dir.txt", 'w') as media_file:
|
|||
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
|
||||
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
|
||||
SOUND_DIR = os.path.join(MEDIA_DIR, "designs", "sounds")
|
||||
###
|
||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
FILE_DIR = os.path.join(THIS_DIR, "files")
|
||||
|
|
|
@ -12,46 +12,43 @@ from manimlib.utils.sounds import play_finish_sound
|
|||
import manimlib.constants
|
||||
|
||||
|
||||
def handle_scene(scene, **config):
|
||||
def open_file_if_needed(file_writer, **config):
|
||||
if config["quiet"]:
|
||||
curr_stdout = sys.stdout
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
if config["show_last_frame"]:
|
||||
scene.save_image(mode=config["saved_image_mode"])
|
||||
open_file = any([
|
||||
config["show_last_frame"],
|
||||
config["open_video_upon_completion"],
|
||||
config["show_file_in_finder"]
|
||||
])
|
||||
if open_file:
|
||||
current_os = platform.system()
|
||||
file_path = None
|
||||
file_paths = []
|
||||
|
||||
if config["show_last_frame"]:
|
||||
file_path = scene.get_image_file_path()
|
||||
else:
|
||||
file_path = scene.get_movie_file_path()
|
||||
if config["file_writer_config"]["save_last_frame"]:
|
||||
file_paths.append(file_writer.get_image_file_path())
|
||||
if config["file_writer_config"]["write_to_movie"]:
|
||||
file_paths.append(file_writer.get_movie_file_path())
|
||||
|
||||
if current_os == "Windows":
|
||||
os.startfile(file_path)
|
||||
else:
|
||||
commands = []
|
||||
for file_path in file_paths:
|
||||
if current_os == "Windows":
|
||||
os.startfile(file_path)
|
||||
else:
|
||||
commands = []
|
||||
if (current_os == "Linux"):
|
||||
commands.append("xdg-open")
|
||||
else: # Assume macOS
|
||||
commands.append("open")
|
||||
|
||||
if (current_os == "Linux"):
|
||||
commands.append("xdg-open")
|
||||
else: # Assume macOS
|
||||
commands.append("open")
|
||||
if config["show_file_in_finder"]:
|
||||
commands.append("-R")
|
||||
|
||||
if config["show_file_in_finder"]:
|
||||
commands.append("-R")
|
||||
commands.append(file_path)
|
||||
|
||||
commands.append(file_path)
|
||||
|
||||
# commands.append("-g")
|
||||
FNULL = open(os.devnull, 'w')
|
||||
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
||||
FNULL.close()
|
||||
# commands.append("-g")
|
||||
FNULL = open(os.devnull, 'w')
|
||||
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
||||
FNULL.close()
|
||||
|
||||
if config["quiet"]:
|
||||
sys.stdout.close()
|
||||
|
@ -128,23 +125,18 @@ def main(config):
|
|||
"camera_config",
|
||||
"frame_duration",
|
||||
"skip_animations",
|
||||
"write_to_movie",
|
||||
"save_pngs",
|
||||
"movie_file_extension",
|
||||
"file_writer_config",
|
||||
"start_at_animation_number",
|
||||
"end_at_animation_number",
|
||||
"output_file_name",
|
||||
"leave_progress_bars",
|
||||
]
|
||||
])
|
||||
if config["save_pngs"]:
|
||||
print("We are going to save a PNG sequence as well...")
|
||||
scene_kwargs["save_pngs"] = True
|
||||
scene_kwargs["pngs_mode"] = config["saved_image_mode"]
|
||||
|
||||
for SceneClass in get_scene_classes(scene_names_to_classes, config):
|
||||
try:
|
||||
handle_scene(SceneClass(**scene_kwargs), **config)
|
||||
# By invoking, this renders the full scene
|
||||
scene = SceneClass(**scene_kwargs)
|
||||
open_file_if_needed(scene.file_writer, **config)
|
||||
if config["sound"]:
|
||||
play_finish_sound()
|
||||
except Exception:
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
from time import sleep
|
||||
import _thread as thread
|
||||
import datetime
|
||||
import inspect
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import warnings
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.creation import Write
|
||||
|
@ -21,12 +14,8 @@ from manimlib.container.container import Container
|
|||
from manimlib.continual_animation.continual_animation import ContinualAnimation
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.svg.tex_mobject import TextMobject
|
||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||
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):
|
||||
|
@ -34,20 +23,12 @@ class Scene(Container):
|
|||
"camera_class": Camera,
|
||||
"camera_config": {},
|
||||
"frame_duration": LOW_QUALITY_FRAME_DURATION,
|
||||
"construct_args": [],
|
||||
"file_writer_config": {},
|
||||
"skip_animations": False,
|
||||
"write_to_movie": False,
|
||||
"save_pngs": False,
|
||||
"pngs_mode": "RGBA",
|
||||
"movie_file_extension": ".mp4",
|
||||
"always_continually_update": False,
|
||||
"random_seed": 0,
|
||||
"start_at_animation_number": None,
|
||||
"end_at_animation_number": None,
|
||||
"livestreaming": False,
|
||||
"to_twitch": False,
|
||||
"twitch_key": None,
|
||||
"output_file_name": None,
|
||||
"leave_progress_bars": False,
|
||||
}
|
||||
|
||||
|
@ -55,43 +36,36 @@ class Scene(Container):
|
|||
# Perhaps allow passing in a non-empty *mobjects parameter?
|
||||
Container.__init__(self, **kwargs)
|
||||
self.camera = self.camera_class(**self.camera_config)
|
||||
self.file_writer = SceneFileWriter(
|
||||
self, **self.file_writer_config,
|
||||
)
|
||||
|
||||
self.mobjects = []
|
||||
self.continual_animations = []
|
||||
self.foreground_mobjects = []
|
||||
self.num_plays = 0
|
||||
self.frame_num = 0
|
||||
self.time = 0
|
||||
self.original_skipping_status = self.skip_animations
|
||||
self.stream_lock = False
|
||||
if self.random_seed is not None:
|
||||
random.seed(self.random_seed)
|
||||
np.random.seed(self.random_seed)
|
||||
|
||||
self.init_audio()
|
||||
self.setup()
|
||||
if self.livestreaming:
|
||||
return None
|
||||
try:
|
||||
self.construct(*self.construct_args)
|
||||
self.construct()
|
||||
except EndSceneEarlyException:
|
||||
if hasattr(self, "writing_process"):
|
||||
self.writing_process.terminate()
|
||||
pass
|
||||
self.tear_down()
|
||||
|
||||
if self.write_to_movie:
|
||||
self.combine_movie_files()
|
||||
self.file_writer.finish()
|
||||
self.print_end_message()
|
||||
|
||||
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)
|
||||
allow_write = not self.skip_animations
|
||||
self.file_writer.begin_animation(allow_write)
|
||||
func(self, *args, **kwargs)
|
||||
self.file_writer.end_animation(allow_write)
|
||||
self.num_plays += 1
|
||||
return wrapper
|
||||
|
||||
|
@ -116,11 +90,6 @@ class Scene(Container):
|
|||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def get_output_file_name(self):
|
||||
if self.output_file_name is not None:
|
||||
return self.output_file_name
|
||||
return str(self)
|
||||
|
||||
def print_end_message(self):
|
||||
print("Played {} animations".format(self.num_plays))
|
||||
|
||||
|
@ -142,40 +111,9 @@ class Scene(Container):
|
|||
def get_attrs(self, *keys):
|
||||
return [getattr(self, key) for key in keys]
|
||||
|
||||
# Sound
|
||||
def init_audio(self):
|
||||
self.includes_sound = False
|
||||
|
||||
def create_audio_segment(self):
|
||||
self.audio_segment = AudioSegment.silent()
|
||||
|
||||
def add_audio_segment(self, new_segment, time_offset=0):
|
||||
if not self.includes_sound:
|
||||
self.includes_sound = True
|
||||
self.create_audio_segment()
|
||||
segment = self.audio_segment
|
||||
overly_time = self.get_time() + time_offset
|
||||
if overly_time < 0:
|
||||
raise Exception("Adding sound at timestamp < 0")
|
||||
|
||||
curr_end = segment.duration_seconds
|
||||
new_end = overly_time + new_segment.duration_seconds
|
||||
diff = new_end - curr_end
|
||||
if diff > 0:
|
||||
segment = segment.append(
|
||||
AudioSegment.silent(int(np.ceil(diff * 1000))),
|
||||
crossfade=0,
|
||||
)
|
||||
self.audio_segment = segment.overlay(
|
||||
new_segment, position=int(1000 * overly_time)
|
||||
)
|
||||
|
||||
def add_sound(self, sound_file, time_offset=0):
|
||||
new_segment = AudioSegment.from_file(sound_file)
|
||||
self.add_audio_segment(new_segment, 0)
|
||||
# TODO, Scene file writer now handles sound
|
||||
|
||||
# Only these methods should touch the camera
|
||||
|
||||
def set_camera(self, camera):
|
||||
self.camera = camera
|
||||
|
||||
|
@ -202,9 +140,9 @@ class Scene(Container):
|
|||
mobjects=None,
|
||||
background=None,
|
||||
include_submobjects=True,
|
||||
dont_update_when_skipping=True,
|
||||
ignore_skipping=True,
|
||||
**kwargs):
|
||||
if self.skip_animations and dont_update_when_skipping:
|
||||
if self.skip_animations and not ignore_skipping:
|
||||
return
|
||||
if mobjects is None:
|
||||
mobjects = list_update(
|
||||
|
@ -522,8 +460,6 @@ class Scene(Container):
|
|||
|
||||
@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
|
||||
|
@ -558,22 +494,11 @@ class Scene(Container):
|
|||
else:
|
||||
self.continual_update(0)
|
||||
|
||||
if self.livestreaming:
|
||||
self.stream_lock = True
|
||||
thread.start_new_thread(self.idle_stream, ())
|
||||
return self
|
||||
|
||||
# TODO
|
||||
def idle_stream(self):
|
||||
while(self.stream_lock):
|
||||
a = datetime.datetime.now()
|
||||
self.update_frame()
|
||||
n_frames = 1
|
||||
frame = self.get_frame()
|
||||
self.add_frames(*[frame] * n_frames)
|
||||
b = datetime.datetime.now()
|
||||
time_diff = (b - a).total_seconds()
|
||||
if time_diff < self.frame_duration:
|
||||
sleep(self.frame_duration - time_diff)
|
||||
self.file_writer.idle_stream()
|
||||
|
||||
def clean_up_animations(self, *animations):
|
||||
for animation in animations:
|
||||
|
@ -638,202 +563,16 @@ class Scene(Container):
|
|||
return self
|
||||
|
||||
def add_frames(self, *frames):
|
||||
self.increment_time(len(frames) * self.frame_duration)
|
||||
if self.skip_animations:
|
||||
return
|
||||
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
|
||||
)
|
||||
self.frame_num = self.frame_num + 1
|
||||
self.writing_process.stdin.write(frame.tostring())
|
||||
|
||||
# Display methods
|
||||
for frame in frames:
|
||||
self.file_writer.write_frame(frame)
|
||||
|
||||
def show_frame(self):
|
||||
self.update_frame(dont_update_when_skipping=False)
|
||||
self.update_frame(ignore_skipping=True)
|
||||
self.get_image().show()
|
||||
|
||||
def get_image_file_path(self, name=None, dont_update=False):
|
||||
sub_dir = "images"
|
||||
output_file_name = self.get_output_file_name()
|
||||
if dont_update:
|
||||
sub_dir = output_file_name
|
||||
path = get_image_output_directory(self.__class__, sub_dir)
|
||||
file_name = add_extension_if_not_present(
|
||||
name or output_file_name, ".png"
|
||||
)
|
||||
return os.path.join(path, file_name)
|
||||
|
||||
def save_image(self, name=None, mode="RGB", dont_update=False):
|
||||
path = self.get_image_file_path(name, dont_update)
|
||||
if not dont_update:
|
||||
self.update_frame(dont_update_when_skipping=False)
|
||||
image = self.get_image()
|
||||
image = image.convert(mode)
|
||||
image.save(path)
|
||||
|
||||
def get_movie_file_path(self, name=None, extension=None):
|
||||
directory = get_movie_output_directory(
|
||||
self.__class__, self.camera_config, self.frame_duration
|
||||
)
|
||||
if extension is None:
|
||||
extension = self.movie_file_extension
|
||||
if name is None:
|
||||
name = self.get_output_file_name()
|
||||
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, self.camera_config, self.frame_duration
|
||||
)
|
||||
|
||||
def open_movie_pipe(self):
|
||||
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.movie_file_path = file_path
|
||||
self.temp_movie_file_path = temp_file_path
|
||||
|
||||
fps = int(1 / self.frame_duration)
|
||||
height = self.camera.get_pixel_height()
|
||||
width = self.camera.get_pixel_width()
|
||||
|
||||
command = [
|
||||
FFMPEG_BIN,
|
||||
'-y', # overwrite output file if it exists
|
||||
'-f', 'rawvideo',
|
||||
'-s', '%dx%d' % (width, height), # size of one frame
|
||||
'-pix_fmt', 'rgba',
|
||||
'-r', str(fps), # frames per second
|
||||
'-i', '-', # The imput comes from a pipe
|
||||
'-c:v', 'h264_nvenc',
|
||||
'-an', # Tells FFMPEG not to expect any audio
|
||||
'-loglevel', 'error',
|
||||
]
|
||||
if self.movie_file_extension == ".mov":
|
||||
# This is if the background of the exported video
|
||||
# should be transparent.
|
||||
command += [
|
||||
'-vcodec', 'qtrle',
|
||||
# '-vcodec', 'png',
|
||||
]
|
||||
else:
|
||||
command += [
|
||||
'-vcodec', 'libx264',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
]
|
||||
if self.livestreaming:
|
||||
if self.to_twitch:
|
||||
command += ['-f', 'flv']
|
||||
command += ['rtmp://live.twitch.tv/app/' + self.twitch_key]
|
||||
else:
|
||||
command += ['-f', 'mpegts']
|
||||
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
|
||||
else:
|
||||
command += [temp_file_path]
|
||||
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
|
||||
shutil.move(
|
||||
self.temp_movie_file_path,
|
||||
self.movie_file_path,
|
||||
)
|
||||
|
||||
def combine_movie_files(self):
|
||||
# Manim renders the scene as many smaller movie files
|
||||
# which are then concatenated to a larger one. The reason
|
||||
# for this is that sometimes video-editing is made easier when
|
||||
# one works with the broken up scene, which effectively has
|
||||
# cuts at all the places you might want. But for viewing
|
||||
# the scene as a whole, one of course wants to see it as a
|
||||
# single piece.
|
||||
partial_movie_file_directory = self.get_partial_movie_directory()
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.movie_file_extension,
|
||||
}
|
||||
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:
|
||||
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:
|
||||
if os.name == 'nt':
|
||||
pf_path = pf_path.replace('\\', '/')
|
||||
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',
|
||||
'-loglevel', 'error',
|
||||
movie_file_path
|
||||
]
|
||||
if not self.includes_sound:
|
||||
commands.insert(-1, '-an')
|
||||
|
||||
combine_process = subprocess.Popen(commands)
|
||||
combine_process.wait()
|
||||
# os.remove(file_list)
|
||||
|
||||
if self.includes_sound:
|
||||
sound_file_path = movie_file_path.replace(
|
||||
self.movie_file_extension, ".wav"
|
||||
)
|
||||
# Makes sure sound file length will match video file
|
||||
self.add_audio_segment(AudioSegment.silent(0))
|
||||
self.audio_segment.export(sound_file_path)
|
||||
temp_file_path = movie_file_path.replace(".", "_temp.")
|
||||
commands = commands = [
|
||||
"ffmpeg",
|
||||
"-i", movie_file_path,
|
||||
"-i", sound_file_path,
|
||||
'-y', # overwrite output file if it exists
|
||||
"-c:v", "copy", "-c:a", "aac",
|
||||
'-loglevel', 'error',
|
||||
"-shortest",
|
||||
"-strict", "experimental",
|
||||
temp_file_path,
|
||||
]
|
||||
subprocess.call(commands)
|
||||
shutil.move(temp_file_path, movie_file_path)
|
||||
# subprocess.call(["rm", self.temp_movie_file_path])
|
||||
subprocess.call(["rm", sound_file_path])
|
||||
|
||||
print("\nAnimation ready at {}\n".format(movie_file_path))
|
||||
|
||||
# TODO, this doesn't belong in Scene, but should be
|
||||
# part of some more specialized subclass optimized
|
||||
# for livestreaming
|
||||
|
|
325
manimlib/scene/scene_file_writer.py
Normal file
325
manimlib/scene/scene_file_writer.py
Normal file
|
@ -0,0 +1,325 @@
|
|||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
import shutil
|
||||
import subprocess
|
||||
import os
|
||||
import _thread as thread
|
||||
from time import sleep
|
||||
import datetime
|
||||
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.constants import STREAMING_IP
|
||||
from manimlib.constants import STREAMING_PORT
|
||||
from manimlib.constants import STREAMING_PROTOCOL
|
||||
from manimlib.constants import VIDEO_DIR
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.file_ops import guarantee_existance
|
||||
from manimlib.utils.file_ops import add_extension_if_not_present
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
|
||||
|
||||
class SceneFileWriter(object):
|
||||
CONFIG = {
|
||||
"write_to_movie": False,
|
||||
# TODO, save_pngs is doing nothing
|
||||
"save_pngs": False,
|
||||
"png_mode": "RGBA",
|
||||
"save_last_frame": False,
|
||||
"movie_file_extension": ".mp4",
|
||||
"livestreaming": False,
|
||||
"to_twitch": False,
|
||||
"twitch_key": None,
|
||||
# Previous output_file_name
|
||||
# TODO, address this in extract_scene et. al.
|
||||
"file_name": None,
|
||||
"output_directory": None,
|
||||
}
|
||||
|
||||
def __init__(self, scene, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.scene = scene
|
||||
self.init_audio()
|
||||
self.init_output_directories()
|
||||
self.stream_lock = False
|
||||
|
||||
# Output directories and files
|
||||
|
||||
def init_output_directories(self):
|
||||
output_directory = self.output_directory or self.get_default_output_directory()
|
||||
file_name = self.file_name or self.get_default_file_name()
|
||||
if self.save_last_frame:
|
||||
image_dir = guarantee_existance(os.path.join(
|
||||
VIDEO_DIR,
|
||||
output_directory,
|
||||
self.get_image_directory(),
|
||||
))
|
||||
self.image_file_path = os.path.join(
|
||||
image_dir,
|
||||
add_extension_if_not_present(file_name, ".png")
|
||||
)
|
||||
if self.write_to_movie:
|
||||
movie_dir = guarantee_existance(os.path.join(
|
||||
output_directory,
|
||||
self.get_movie_directory(),
|
||||
))
|
||||
self.movie_file_path = os.path.join(
|
||||
movie_dir,
|
||||
add_extension_if_not_present(
|
||||
file_name, self.movie_file_extension
|
||||
)
|
||||
)
|
||||
self.partial_movie_directory = guarantee_existance(os.path.join(
|
||||
movie_dir,
|
||||
self.get_partial_movie_directory(),
|
||||
file_name,
|
||||
))
|
||||
|
||||
def get_default_output_directory(self):
|
||||
scene_module = self.scene.__class__.__module__
|
||||
return scene_module.replace(".", os.path.sep)
|
||||
|
||||
def get_default_file_name(self):
|
||||
return self.scene.__class__.__name__
|
||||
|
||||
def get_movie_directory(self):
|
||||
pixel_height = self.scene.camera.pixel_height
|
||||
frame_duration = self.scene.frame_duration
|
||||
return "{}p{}".format(
|
||||
pixel_height, int(1.0 / frame_duration)
|
||||
)
|
||||
|
||||
def get_image_directory(self):
|
||||
return "images"
|
||||
|
||||
def get_partial_movie_directory(self):
|
||||
return "partial_movie_directory"
|
||||
|
||||
# Sound
|
||||
# TODO, make work with Scene
|
||||
def init_audio(self):
|
||||
self.includes_sound = False
|
||||
|
||||
def create_audio_segment(self):
|
||||
self.audio_segment = AudioSegment.silent()
|
||||
|
||||
def add_audio_segment(self, new_segment, time_offset=0):
|
||||
if not self.includes_sound:
|
||||
self.includes_sound = True
|
||||
self.create_audio_segment()
|
||||
segment = self.audio_segment
|
||||
overly_time = self.get_time() + time_offset
|
||||
if overly_time < 0:
|
||||
raise Exception("Adding sound at timestamp < 0")
|
||||
|
||||
curr_end = segment.duration_seconds
|
||||
new_end = overly_time + new_segment.duration_seconds
|
||||
diff = new_end - curr_end
|
||||
if diff > 0:
|
||||
segment = segment.append(
|
||||
AudioSegment.silent(int(np.ceil(diff * 1000))),
|
||||
crossfade=0,
|
||||
)
|
||||
self.audio_segment = segment.overlay(
|
||||
new_segment, position=int(1000 * overly_time)
|
||||
)
|
||||
|
||||
def add_sound(self, sound_file, time_offset=0):
|
||||
new_segment = AudioSegment.from_file(sound_file)
|
||||
self.add_audio_segment(new_segment, 0)
|
||||
|
||||
# Directory getters
|
||||
def get_image_file_path(self):
|
||||
return self.image_file_path
|
||||
|
||||
def get_next_partial_movie_path(self):
|
||||
result = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"{:05}{}".format(
|
||||
self.scene.num_plays,
|
||||
self.movie_file_extension,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def get_movie_file_path(self):
|
||||
return self.movie_file_path
|
||||
|
||||
# Writers
|
||||
def write_frame(self, frame):
|
||||
if self.write_to_movie:
|
||||
self.writing_process.stdin.write(frame.tostring())
|
||||
|
||||
def save_image(self, image):
|
||||
file_path = self.get_image_file_path()
|
||||
image.save(file_path)
|
||||
self.print_file_ready_message(file_path)
|
||||
|
||||
def begin_animation(self, allow_write=False):
|
||||
if self.write_to_movie and allow_write:
|
||||
self.open_movie_pipe()
|
||||
if self.livestreaming:
|
||||
self.stream_lock = False
|
||||
|
||||
def end_animation(self, allow_write=False):
|
||||
if self.write_to_movie and allow_write:
|
||||
self.close_movie_pipe()
|
||||
if self.livestreaming:
|
||||
self.stream_lock = True
|
||||
thread.start_new_thread(self.idle_stream, ())
|
||||
|
||||
def idle_stream(self):
|
||||
while self.stream_lock:
|
||||
a = datetime.datetime.now()
|
||||
self.update_frame()
|
||||
n_frames = 1
|
||||
frame = self.get_frame()
|
||||
self.add_frames(*[frame] * n_frames)
|
||||
b = datetime.datetime.now()
|
||||
time_diff = (b - a).total_seconds()
|
||||
if time_diff < self.frame_duration:
|
||||
sleep(self.frame_duration - time_diff)
|
||||
|
||||
def finish(self):
|
||||
if self.write_to_movie:
|
||||
if hasattr(self, "writing_process"):
|
||||
self.writing_process.terminate()
|
||||
self.combine_movie_files()
|
||||
if self.save_last_frame:
|
||||
self.scene.update_frame(ignore_skipping=True)
|
||||
self.save_image(self.scene.get_image())
|
||||
|
||||
def open_movie_pipe(self):
|
||||
file_path = self.get_next_partial_movie_path()
|
||||
temp_file_path = file_path.replace(".", "_temp.")
|
||||
|
||||
self.partial_movie_file_path = file_path
|
||||
self.temp_partial_movie_file_path = temp_file_path
|
||||
|
||||
fps = int(1 / self.scene.frame_duration)
|
||||
height = self.scene.camera.get_pixel_height()
|
||||
width = self.scene.camera.get_pixel_width()
|
||||
|
||||
command = [
|
||||
FFMPEG_BIN,
|
||||
'-y', # overwrite output file if it exists
|
||||
'-f', 'rawvideo',
|
||||
'-s', '%dx%d' % (width, height), # size of one frame
|
||||
'-pix_fmt', 'rgba',
|
||||
'-r', str(fps), # frames per second
|
||||
'-i', '-', # The imput comes from a pipe
|
||||
'-c:v', 'h264_nvenc',
|
||||
'-an', # Tells FFMPEG not to expect any audio
|
||||
'-loglevel', 'error',
|
||||
]
|
||||
if self.movie_file_extension == ".mov":
|
||||
# This is if the background of the exported video
|
||||
# should be transparent.
|
||||
command += [
|
||||
'-vcodec', 'qtrle',
|
||||
# '-vcodec', 'png',
|
||||
]
|
||||
else:
|
||||
command += [
|
||||
'-vcodec', 'libx264',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
]
|
||||
if self.livestreaming:
|
||||
if self.to_twitch:
|
||||
command += ['-f', 'flv']
|
||||
command += ['rtmp://live.twitch.tv/app/' + self.twitch_key]
|
||||
else:
|
||||
command += ['-f', 'mpegts']
|
||||
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
|
||||
else:
|
||||
command += [temp_file_path]
|
||||
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
|
||||
shutil.move(
|
||||
self.temp_partial_movie_file_path,
|
||||
self.partial_movie_file_path,
|
||||
)
|
||||
|
||||
def combine_movie_files(self):
|
||||
# Manim renders the scene as many smaller movie files
|
||||
# which are then concatenated to a larger one. The reason
|
||||
# for this is that sometimes video-editing is made easier when
|
||||
# one works with the broken up scene, which effectively has
|
||||
# cuts at all the places you might want. But for viewing
|
||||
# the scene as a whole, one of course wants to see it as a
|
||||
# single piece.
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.movie_file_extension,
|
||||
}
|
||||
if self.scene.start_at_animation_number is not None:
|
||||
kwargs["min_index"] = self.start_at_animation_number
|
||||
if self.scene.end_at_animation_number is not None:
|
||||
kwargs["max_index"] = self.end_at_animation_number
|
||||
else:
|
||||
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
|
||||
partial_movie_files = get_sorted_integer_files(
|
||||
self.partial_movie_directory,
|
||||
**kwargs
|
||||
)
|
||||
# Write a file partial_file_list.txt containing all
|
||||
# partial movie files
|
||||
file_list = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"partial_movie_file_list.txt"
|
||||
)
|
||||
with open(file_list, 'w') as fp:
|
||||
for pf_path in partial_movie_files:
|
||||
if os.name == 'nt':
|
||||
pf_path = pf_path.replace('\\', '/')
|
||||
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',
|
||||
'-loglevel', 'error',
|
||||
movie_file_path
|
||||
]
|
||||
if not self.includes_sound:
|
||||
commands.insert(-1, '-an')
|
||||
|
||||
combine_process = subprocess.Popen(commands)
|
||||
combine_process.wait()
|
||||
# os.remove(file_list)
|
||||
|
||||
if self.includes_sound:
|
||||
sound_file_path = movie_file_path.replace(
|
||||
self.movie_file_extension, ".wav"
|
||||
)
|
||||
# Makes sure sound file length will match video file
|
||||
self.add_audio_segment(AudioSegment.silent(0))
|
||||
self.audio_segment.export(sound_file_path)
|
||||
temp_file_path = movie_file_path.replace(".", "_temp.")
|
||||
commands = commands = [
|
||||
"ffmpeg",
|
||||
"-i", movie_file_path,
|
||||
"-i", sound_file_path,
|
||||
'-y', # overwrite output file if it exists
|
||||
"-c:v", "copy", "-c:a", "aac",
|
||||
'-loglevel', 'error',
|
||||
"-shortest",
|
||||
"-strict", "experimental",
|
||||
temp_file_path,
|
||||
]
|
||||
subprocess.call(commands)
|
||||
shutil.move(temp_file_path, movie_file_path)
|
||||
subprocess.call(["rm", sound_file_path])
|
||||
|
||||
self.print_file_ready_message(movie_file_path)
|
||||
|
||||
def print_file_ready_message(self, file_path):
|
||||
print("\nFile ready at {}\n".format(file_path))
|
|
@ -1,8 +1,6 @@
|
|||
import os
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import VIDEO_DIR
|
||||
|
||||
|
||||
def add_extension_if_not_present(file_name, extension):
|
||||
# This could conceivably be smarter about handling existing differing extensions
|
||||
|
@ -18,38 +16,38 @@ def guarantee_existance(path):
|
|||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def get_scene_output_directory(scene_class):
|
||||
return guarantee_existance(os.path.join(
|
||||
VIDEO_DIR,
|
||||
scene_class.__module__.replace(".", os.path.sep)
|
||||
))
|
||||
# def get_scene_output_directory(scene_class):
|
||||
# return guarantee_existance(os.path.join(
|
||||
# VIDEO_DIR,
|
||||
# scene_class.__module__.replace(".", os.path.sep)
|
||||
# ))
|
||||
|
||||
|
||||
def get_movie_output_directory(scene_class, camera_config, frame_duration):
|
||||
directory = get_scene_output_directory(scene_class)
|
||||
sub_dir = "%dp%d" % (
|
||||
camera_config["pixel_height"],
|
||||
int(1.0 / frame_duration)
|
||||
)
|
||||
return guarantee_existance(os.path.join(directory, sub_dir))
|
||||
# def get_movie_output_directory(scene_class, camera_config, frame_duration):
|
||||
# directory = get_scene_output_directory(scene_class)
|
||||
# sub_dir = "%dp%d" % (
|
||||
# camera_config["pixel_height"],
|
||||
# int(1.0 / frame_duration)
|
||||
# )
|
||||
# return guarantee_existance(os.path.join(directory, sub_dir))
|
||||
|
||||
|
||||
def get_partial_movie_output_directory(scene, camera_config, frame_duration):
|
||||
directory = get_movie_output_directory(
|
||||
scene.__class__, camera_config, frame_duration
|
||||
)
|
||||
return guarantee_existance(
|
||||
os.path.join(
|
||||
directory,
|
||||
"partial_movie_files",
|
||||
scene.get_output_file_name(),
|
||||
)
|
||||
)
|
||||
# def get_partial_movie_output_directory(scene, camera_config, frame_duration):
|
||||
# directory = get_movie_output_directory(
|
||||
# scene.__class__, camera_config, frame_duration
|
||||
# )
|
||||
# return guarantee_existance(
|
||||
# os.path.join(
|
||||
# directory,
|
||||
# "partial_movie_files",
|
||||
# scene.get_output_file_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_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,
|
|
@ -8,8 +8,7 @@ from manimlib.constants import PRODUCTION_QUALITY_CAMERA_CONFIG
|
|||
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_sorted_integer_files
|
||||
from manimlib.utils.file_ops import get_movie_output_directory
|
||||
|
||||
|
||||
def get_sorted_scene_classes(module_name):
|
||||
|
|
Loading…
Add table
Reference in a new issue