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.
|
||||
|
||||
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)
|
||||
else:
|
||||
mobject = mob_or_text
|
||||
|
||||
if "run_time" not in kwargs:
|
||||
self.establish_run_time(mobject)
|
||||
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)
|
||||
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"
|
||||
|
|
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 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
|
||||
|
|
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