mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Merge pull request #336 from mertyildiran/master
Live streaming and interactive shell
This commit is contained in:
commit
169c443e65
6 changed files with 121 additions and 2 deletions
15
README.md
15
README.md
|
@ -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).
|
||||||
|
|
|
@ -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:
|
||||||
|
|
10
constants.py
10
constants.py
|
@ -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
31
manim.py
Normal 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)
|
|
@ -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,18 +653,36 @@ class Scene(Container):
|
||||||
'-vcodec', 'libx264',
|
'-vcodec', 'libx264',
|
||||||
'-pix_fmt', 'yuv420p',
|
'-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, shell=True)
|
||||||
self.writing_process = sp.Popen(command, stdin=sp.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 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
13
stream_starter.py
Normal 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!")
|
Loading…
Add table
Reference in a new issue