Merge pull request #336 from mertyildiran/master

Live streaming and interactive shell
This commit is contained in:
Grant Sanderson 2018-11-18 08:41:35 -08:00 committed by GitHub
commit 169c443e65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 2 deletions

View file

@ -69,3 +69,18 @@ a Dockerfile provided.
On a Windows system, make sure to replace `$PWD` with an absolute path to manim. 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/). 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]
<scene.scene.Scene object at 0x7f0756d5a8d0>
```
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).

View file

@ -98,6 +98,7 @@ class Write(DrawBorderThenFill):
mobject = TextMobject(mob_or_text) mobject = TextMobject(mob_or_text)
else: else:
mobject = mob_or_text mobject = mob_or_text
if "run_time" not in kwargs: if "run_time" not in kwargs:
self.establish_run_time(mobject) self.establish_run_time(mobject)
if "lag_factor" not in kwargs: if "lag_factor" not in kwargs:

View file

@ -213,3 +213,13 @@ PALETTE = list(COLOR_MAP.values())
locals().update(COLOR_MAP) locals().update(COLOR_MAP)
for name in [s for s in list(COLOR_MAP.keys()) if s.endswith("_C")]: for name in [s for s in list(COLOR_MAP.keys()) if s.endswith("_C")]:
locals()[name.replace("_C", "")] = locals()[name] 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"

31
manim.py Normal file
View file

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

View file

@ -7,13 +7,18 @@ import random
import shutil import shutil
import subprocess as sp import subprocess as sp
import warnings 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 tqdm import tqdm as ProgressDisplay
from constants import * from constants import *
from animation.animation import Animation from animation.animation import Animation
from animation.transform import MoveToTarget from animation.transform import MoveToTarget, ApplyMethod
from camera.camera import Camera from camera.camera import Camera
from continual_animation.continual_animation import ContinualAnimation from continual_animation.continual_animation import ContinualAnimation
from mobject.mobject import Mobject 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 container.container import Container
from mobject.svg.tex_mobject import TextMobject
from animation.creation import Write
import datetime
class Scene(Container): class Scene(Container):
CONFIG = { CONFIG = {
@ -58,6 +66,7 @@ class Scene(Container):
self.frame_num = 0 self.frame_num = 0
self.current_scene_time = 0 self.current_scene_time = 0
self.original_skipping_status = self.skip_animations self.original_skipping_status = self.skip_animations
self.stream_lock = False
if self.name is None: if self.name is None:
self.name = self.__class__.__name__ self.name = self.__class__.__name__
if self.random_seed is not None: if self.random_seed is not None:
@ -67,6 +76,8 @@ class Scene(Container):
self.setup() self.setup()
if self.write_to_movie: if self.write_to_movie:
self.open_movie_pipe() self.open_movie_pipe()
if IS_LIVE_STREAMING:
return None
try: try:
self.construct(*self.construct_args) self.construct(*self.construct_args)
except EndSceneEarlyException: except EndSceneEarlyException:
@ -453,6 +464,8 @@ class Scene(Container):
raise EndSceneEarlyException() raise EndSceneEarlyException()
def play(self, *args, **kwargs): def play(self, *args, **kwargs):
if IS_LIVE_STREAMING:
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
@ -489,8 +502,25 @@ class Scene(Container):
else: else:
self.continual_update(0) self.continual_update(0)
self.num_plays += 1 self.num_plays += 1
if IS_LIVE_STREAMING:
self.stream_lock = True
thread.start_new_thread(self.idle_stream, ())
return self 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): def clean_up_animations(self, *animations):
for animation in animations: for animation in animations:
animation.clean_up(self) animation.clean_up(self)
@ -608,6 +638,7 @@ class Scene(Container):
'-pix_fmt', 'rgba', '-pix_fmt', 'rgba',
'-r', str(fps), # frames per second '-r', str(fps), # frames per second
'-i', '-', # The imput comes from a pipe '-i', '-', # The imput comes from a pipe
'-c:v', 'h264_nvenc',
'-an', # Tells FFMPEG not to expect any audio '-an', # Tells FFMPEG not to expect any audio
'-loglevel', 'error', '-loglevel', 'error',
] ]
@ -622,6 +653,14 @@ class Scene(Container):
'-vcodec', 'libx264', '-vcodec', 'libx264',
'-pix_fmt', 'yuv420p', '-pix_fmt', 'yuv420p',
] ]
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] command += [temp_file_path]
# self.writing_process = sp.Popen(command, stdin=sp.PIPE, shell=True) # self.writing_process = sp.Popen(command, stdin=sp.PIPE, shell=True)
self.writing_process = sp.Popen(command, stdin=sp.PIPE) self.writing_process = sp.Popen(command, stdin=sp.PIPE)
@ -629,11 +668,21 @@ class Scene(Container):
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 IS_LIVE_STREAMING:
return True
if os.name == 'nt': if os.name == 'nt':
shutil.move(*self.args_to_rename_file) shutil.move(*self.args_to_rename_file)
else: else:
os.rename(*self.args_to_rename_file) 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): class EndSceneEarlyException(Exception):
pass pass

13
stream_starter.py Normal file
View file

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