diff --git a/README.md b/README.md index b2bed224..c090d34f 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,18 @@ a Dockerfile provided. On a Windows system, make sure to replace `$PWD` with an absolute path to manim. Note that the shipped Docker image contains the bare requirements to run manim. To transform the Docker container into a fully-functioning development environment, you will have to edit your personal Dockerfile a bit. For a guide to create your own personal Docker image, consult [this guide to Dockerfiles](https://www.howtoforge.com/tutorial/how-to-create-docker-images-with-dockerfile/). + +## Live Streaming + +To live stream your animations, simply assign `IS_LIVE_STREAMING = True` in `constants.py` file and from your Python Interactive Shell (`python3`) import the stream starter with `from stream_starter import *` while under the project directory. This will provide a clean interactive shell to enter your commands. `manim` object is a `Manim()` instance so as soon as you play an animation with `manim.play()` your stream will start. A video player will pop-up and you can broadcast that video using [OBS Studio](https://obsproject.com/) which is the most practical way of streaming with this math animation library. An example: + +``` +>>> from stream_starter import * +YOUR STREAM IS READY! +>>> circle = Circle() +>>> manim.play(ShowCreation(circle)) +Animation 0: ShowCreationCircle: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 60/60 [00:01<00:00, 37.30it/s] + +``` + +It is also possible to stream directly to Twitch. To do that simply assign `IS_STREAMING_TO_TWITCH = True` in `constants.py` file and put your Twitch Stream Key to `TWITCH_STREAM_KEY = "YOUR_STREAM_KEY"` and when you follow the above example the stream will directly start on your Twitch channel(with no audio support). diff --git a/animation/creation.py b/animation/creation.py index 2c82c232..23530d71 100644 --- a/animation/creation.py +++ b/animation/creation.py @@ -98,6 +98,7 @@ class Write(DrawBorderThenFill): mobject = TextMobject(mob_or_text) else: mobject = mob_or_text + if "run_time" not in kwargs: self.establish_run_time(mobject) if "lag_factor" not in kwargs: diff --git a/constants.py b/constants.py index 0a6ae11c..078f103b 100644 --- a/constants.py +++ b/constants.py @@ -213,3 +213,13 @@ PALETTE = list(COLOR_MAP.values()) locals().update(COLOR_MAP) for name in [s for s in list(COLOR_MAP.keys()) if s.endswith("_C")]: locals()[name.replace("_C", "")] = locals()[name] + +# Streaming related configurations +IS_LIVE_STREAMING = False +LIVE_STREAM_NAME = "LiveStream" +IS_STREAMING_TO_TWITCH = False +TWITCH_STREAM_KEY = "YOUR_STREAM_KEY" +STREAMING_PROTOCOL = "tcp" +STREAMING_IP = "127.0.0.1" +STREAMING_PORT = "2000" +STREAMING_CLIENT = "ffplay" diff --git a/manim.py b/manim.py new file mode 100644 index 00000000..495db647 --- /dev/null +++ b/manim.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +from constants import * +from scene.scene import Scene + + +class Manim(): + + def __new__(cls): + kwargs = { + "scene_name": LIVE_STREAM_NAME, + "open_video_upon_completion": False, + "show_file_in_finder": False, + # By default, write to file + "write_to_movie": True, + "show_last_frame": False, + "save_pngs": False, + # If -t is passed in (for transparent), this will be RGBA + "saved_image_mode": "RGB", + "movie_file_extension": ".mp4", + "quiet": True, + "ignore_waits": False, + "write_all": False, + "name": LIVE_STREAM_NAME, + "start_at_animation_number": 0, + "end_at_animation_number": None, + "skip_animations": False, + "camera_config": HIGH_QUALITY_CAMERA_CONFIG, + "frame_duration": MEDIUM_QUALITY_FRAME_DURATION, + } + return Scene(**kwargs) diff --git a/scene/scene.py b/scene/scene.py index 213475c9..d4c8475a 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -7,13 +7,18 @@ import random import shutil import subprocess as sp import warnings +from time import sleep +try: + import thread # Low-level threading API (Python 2.7) +except ImportError: + import _thread as thread # Low-level threading API (Python 3.x) from tqdm import tqdm as ProgressDisplay from constants import * from animation.animation import Animation -from animation.transform import MoveToTarget +from animation.transform import MoveToTarget, ApplyMethod from camera.camera import Camera from continual_animation.continual_animation import ContinualAnimation from mobject.mobject import Mobject @@ -24,6 +29,9 @@ from utils.output_directory_getters import get_image_output_directory from container.container import Container +from mobject.svg.tex_mobject import TextMobject +from animation.creation import Write +import datetime class Scene(Container): CONFIG = { @@ -58,6 +66,7 @@ class Scene(Container): self.frame_num = 0 self.current_scene_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: @@ -67,6 +76,8 @@ class Scene(Container): self.setup() if self.write_to_movie: self.open_movie_pipe() + if IS_LIVE_STREAMING: + return None try: self.construct(*self.construct_args) except EndSceneEarlyException: @@ -453,6 +464,8 @@ class Scene(Container): raise EndSceneEarlyException() def play(self, *args, **kwargs): + if IS_LIVE_STREAMING: + self.stream_lock = False if len(args) == 0: warnings.warn("Called Scene.play with no animations") return @@ -489,8 +502,25 @@ class Scene(Container): else: self.continual_update(0) self.num_plays += 1 + + if IS_LIVE_STREAMING: + self.stream_lock = True + thread.start_new_thread(self.idle_stream, ()) + return self + 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 clean_up_animations(self, *animations): for animation in animations: animation.clean_up(self) @@ -608,6 +638,7 @@ class Scene(Container): '-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', ] @@ -622,18 +653,36 @@ class Scene(Container): '-vcodec', 'libx264', '-pix_fmt', 'yuv420p', ] - command += [temp_file_path] + if IS_LIVE_STREAMING: + if IS_STREAMING_TO_TWITCH: + command += ['-f', 'flv'] + command += ['rtmp://live.twitch.tv/app/' + TWITCH_STREAM_KEY] + else: + command += ['-f', 'mpegts'] + 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) def close_movie_pipe(self): self.writing_process.stdin.close() self.writing_process.wait() + if IS_LIVE_STREAMING: + return True if os.name == 'nt': shutil.move(*self.args_to_rename_file) else: os.rename(*self.args_to_rename_file) + def tex(self, latex): + eq = TextMobject(latex) + anims = [] + anims.append(Write(eq)) + for mobject in self.mobjects: + anims.append(ApplyMethod(mobject.shift,2*UP)) + self.play(*anims) + class EndSceneEarlyException(Exception): pass diff --git a/stream_starter.py b/stream_starter.py new file mode 100644 index 00000000..17794c4c --- /dev/null +++ b/stream_starter.py @@ -0,0 +1,13 @@ +from big_ol_pile_of_manim_imports import * +import subprocess +from time import sleep +from manim import Manim + +if not IS_STREAMING_TO_TWITCH: + FNULL = open(os.devnull, 'w') + subprocess.Popen([STREAMING_CLIENT, STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT + '?listen'], stdout=FNULL, stderr=FNULL) + sleep(3) + +manim = Manim() + +print("YOUR STREAM IS READY!")