-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Make livestreaming a thing. #778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7725ec2
a5523e2
ab07add
e275d11
b4f1fce
8341da8
1ed3f68
fd14d66
e8ba6f3
4a4be30
49e8759
22bab46
2254be0
5f77979
0d08bc7
3c1fd9b
8d5507a
75eeb79
a6d1fa3
44ce2d1
eb7a137
4deae04
995db49
7490898
6f4ce3b
3a67a2f
4c04a9f
7b22388
4f821bd
3437a19
9a381f6
42533e9
7c9cff1
5292be3
33bc6f3
2094a13
2df207e
7f27a50
173fc05
24e6c67
502646e
866933b
e52b4b9
38edbc8
2cb6459
a501084
0342be0
9671ad1
78fc7fb
7e21430
a7384da
6c9a140
9dc7e16
74cded9
fc37ec2
6f6ea55
be04f30
cd612ff
f311539
ea5e2c7
034a4d1
553e309
4267ae8
3903bca
0e9ef90
424a671
71dfb58
7f78dd8
5a93fb7
9677699
b68b93a
289d11e
962a0f7
945621b
350da82
8a90a9f
c8821a6
c18216e
f22322f
d0ead32
eec5f4b
d05600e
2c60617
a48ca1e
066f195
1c08a67
ed2d86f
9c23094
5b6285e
ce8cbfd
6135afc
6c6a875
212e894
4e33544
a789aff
e16c556
d76772e
aaca74a
040281f
c2782db
1b4e80b
000539c
08dc895
0a8b49a
9584944
973ae77
b011fc4
9f352c8
97b41c8
2312d9d
1baeea9
b353209
07bd4c8
85a0c8b
a5e0dc7
434155d
ce6c76a
f6f5553
eb7cf71
eb8430f
4810b56
638a03b
d600098
48d2009
82a7960
6b61eae
ad36871
7daf498
449954b
e347ed9
9ef7af2
069b771
3e7d9d8
d7313db
e796f60
6e86313
9b4bd67
cda56b8
d514586
ffa1cd1
3bd9d1c
44ea5cf
7719958
f0e6df6
c6e35a7
eb76955
515f836
641d68e
67f6f14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -353,12 +353,12 @@ A list of all config options | |
'frame_y_radius', 'from_animation_number', 'images_dir', 'input_file', | ||
'leave_progress_bars', 'left_side', 'log_dir', 'log_to_file', | ||
'max_files_cached', 'media_dir', 'media_width', 'movie_file_extension', 'output_file', | ||
'partial_movie_dir', 'pixel_height', 'pixel_width', 'plugins', 'png_mode', | ||
'partial_movie_dir', 'pixel_height', 'pixel_width', 'plugins', 'png_mode', | ||
'preview', 'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame', | ||
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir', | ||
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'streaming_dir', 'tex_dir', | ||
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent', | ||
'upto_animation_number', 'use_opengl_renderer', 'use_webgl_renderer', 'verbosity', | ||
'video_dir', 'webgl_renderer_path', 'webgl_updater_fps', 'write_all', | ||
'upto_animation_number', 'use_opengl_renderer', 'use_webgl_renderer', 'verbosity', | ||
'video_dir', 'webgl_renderer_path', 'webgl_updater_fps', 'write_all', | ||
'write_to_movie'] | ||
|
||
|
||
|
@@ -441,6 +441,8 @@ A list of all CLI flags | |
--config_file CONFIG_FILE | ||
Specify the configuration file | ||
--custom_folders Use the folders defined in the [custom_folders] section of the config file to define the output folder structure | ||
--livestream Run in streaming mode | ||
--use-ipython Use IPython for streaming | ||
Comment on lines
+444
to
+445
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this section exists after the click merge |
||
-v {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --verbosity {DEBUG,INFO,WARNING,ERROR,CRITICAL} | ||
Verbosity level. Also changes the ffmpeg log level unless the latter is specified in the config | ||
--version Print the current version of Manim you are using | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
) | ||
|
||
from manim._config.main_utils import parse_args | ||
from manim.stream_starter import livestream | ||
|
||
|
||
def main(): | ||
|
@@ -44,6 +45,11 @@ def main(): | |
|
||
else: | ||
config.digest_args(args) | ||
|
||
if args.livestream: | ||
livestream(use_ipython=args.use_ipython) | ||
return | ||
|
||
Comment on lines
+48
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would need to move to cli/render/commands.py -- just keep the upstream changes for this file entirely |
||
input_file = config.get_dir("input_file") | ||
|
||
if config["use_opengl_renderer"]: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -190,6 +190,8 @@ def _parse_args_no_subcmd(args: list) -> argparse.Namespace: | |
|
||
parser.add_argument( | ||
"file", | ||
nargs="?", | ||
default="", | ||
NeoPlato marked this conversation as resolved.
Show resolved
Hide resolved
|
||
help="Path to file holding the python code for the scene", | ||
) | ||
parser.add_argument( | ||
|
@@ -449,6 +451,20 @@ def _parse_args_no_subcmd(args: list) -> argparse.Namespace: | |
"section of the config file to define the output folder structure", | ||
) | ||
|
||
# Overrides default false streaming so streaming can happen | ||
parser.add_argument( | ||
"--livestream", | ||
action="store_true", | ||
help="Run in streaming mode", | ||
) | ||
|
||
# Optional use of IPython for streaming | ||
parser.add_argument( | ||
"--use-ipython", | ||
action="store_true", | ||
help="Use IPython for streaming", | ||
) | ||
|
||
Comment on lines
+454
to
+467
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is entire file doesn't even exist anymore. these options would need to be moved to one of the four files I mentioned for categories under the render command. Argparse has |
||
# Specify the verbosity | ||
parser.add_argument( | ||
"-v", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -283,6 +283,7 @@ class MyScene(Scene): | |
"save_pngs", | ||
"scene_names", | ||
"show_in_file_browser", | ||
"streaming_dir", | ||
"sound", | ||
"tex_dir", | ||
"tex_template_file", | ||
|
@@ -547,6 +548,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig": | |
"movie_file_extension", | ||
"background_color", | ||
"webgl_renderer_path", | ||
"streaming_dir", | ||
]: | ||
setattr(self, key, parser["CLI"].get(key, fallback="", raw=True)) | ||
|
||
|
@@ -581,6 +583,23 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig": | |
if val: | ||
setattr(self, "media_width", val) | ||
|
||
streaming_config = { | ||
opt: parser["streaming"].get(opt, fallback="", raw=True) | ||
for opt in [ | ||
"streaming_client", | ||
"streaming_protocol", | ||
"streaming_ip", | ||
"streaming_port", | ||
] | ||
} | ||
url = parser["streaming"].get("streaming_url", fallback="", raw=True) | ||
sdp_name = parser["streaming"].get("sdp_name", fallback="", raw=True) | ||
streaming_config["streaming_url"] = url.format(**streaming_config) | ||
streaming_config["sdp_path"] = os.path.join( | ||
self.get_dir("streaming_dir"), sdp_name.format(**streaming_config) | ||
) | ||
Comment on lines
+586
to
+600
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine, but the file still needs to resolve conflicts -- keep both changes |
||
self.streaming_config = streaming_config | ||
|
||
return self | ||
|
||
def digest_args(self, args: argparse.Namespace) -> "ManimConfig": | ||
|
@@ -705,7 +724,7 @@ def digest_args(self, args: argparse.Namespace) -> "ManimConfig": | |
"partial_movie_dir", | ||
]: | ||
self[opt] = self._parser["custom_folders"].get(opt, raw=True) | ||
# --media_dir overrides the deaful.cfg file | ||
# --media_dir overrides the default.cfg file | ||
if hasattr(args, "media_dir") and args.media_dir: | ||
self.media_dir = args.media_dir | ||
|
||
|
@@ -1224,6 +1243,7 @@ def get_dir(self, key: str, **kwargs: str) -> Path: | |
"input_file", | ||
"output_file", | ||
"partial_movie_dir", | ||
"streaming_dir", | ||
] | ||
if key not in dirs: | ||
raise KeyError( | ||
|
@@ -1299,6 +1319,12 @@ def _set_dir(self, key: str, val: typing.Union[str, Path]): | |
doc="Directory to place partial movie files (no flag). See :meth:`ManimConfig.get_dir`.", | ||
) | ||
|
||
streaming_dir = property( | ||
lambda self: self._d["streaming_dir"], | ||
lambda self, val: self._set_dir("streaming_dir", val), | ||
doc="Directory to have streamed files. See :meth:`ManimConfig.get_dir`.", | ||
) | ||
behackl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
custom_folders = property( | ||
lambda self: self._d["custom_folders"], | ||
lambda self, val: self._set_boolean("custom_folders", val), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from manim.renderer.cairo_renderer import CairoRenderer | ||
from manim._config import config, logger | ||
from manim.scene.stream_file_writer import StreamFileWriter | ||
|
||
from manim.utils.hashing import get_hash_from_play_call | ||
|
||
|
||
class StreamCairoRenderer(CairoRenderer): | ||
def init_scene(self, scene): | ||
"""For compatibility with the __init__ from scene that's not being | ||
directly overridden | ||
""" | ||
self.file_writer = StreamFileWriter(self) | ||
|
||
def play(self, scene, *args, **kwargs): | ||
"""Meant to attach some things | ||
|
||
Args: | ||
scene ([type]): [description] | ||
""" | ||
scene.compile_animation_data(*args, **kwargs) | ||
if not config["disable_caching"] and not self.skip_animations: | ||
hash_current_animation = get_hash_from_play_call( | ||
scene, self.camera, scene.animations, scene.mobjects | ||
) | ||
else: | ||
hash_current_animation = f"uncached_{self.num_plays:05}" | ||
self.file_writer.add_partial_movie_file(hash_current_animation) | ||
super().play(scene, *args, **kwargs) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
from abc import ABCMeta | ||
|
||
import os | ||
import subprocess | ||
|
||
from .. import config, logger | ||
from ..constants import FFMPEG_BIN | ||
from .scene import Scene | ||
from .scene_file_writer import SceneFileWriter | ||
from ..utils.file_ops import guarantee_existence | ||
|
||
|
||
class StreamFileWriter(SceneFileWriter): | ||
naveen521kk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Specialized file writer for streaming. | ||
|
||
Takes a good portion of its implementation from `:class:~.SceneFileWriter` | ||
but changes enough of it to redirect output directories and the final | ||
request to the streaming protocol. | ||
|
||
.. seealso:: | ||
|
||
:func:`~.stream_starter.livestream` | ||
:class:`~.SceneFileWriter` | ||
|
||
""" | ||
|
||
def __init__(self, renderer): | ||
super().__init__(renderer, "") | ||
vars(self).update(config.streaming_config) | ||
path = os.path.join(config.get_dir("streaming_dir"), "clips") | ||
self.partial_movie_directory = os.path.relpath(guarantee_existence(path)) | ||
|
||
def init_output_directories(self, scene_name): | ||
"""Overridden to avoid creation of unnecessary output directories.""" | ||
pass | ||
|
||
@property | ||
def file_path(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What file path is this for? Trying to access it yields >>> manim.renderer.file_writer.file_path
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/behackl/code/manim/manim/scene/stream_file_writer.py", line 31, in file_path
return self.partial_movie_files[-1]
IndexError: list index out of range There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Either please remove this method if it is not used, or add a one-line docstring if it is used.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self for this one too |
||
"""Returns the path of the most recent animation generated by the | ||
class. This is insignificant to the base class which has its own ways | ||
of acquiring this path. | ||
""" | ||
self.partial_movie_files = list(filter(lambda item: item is not None, self.partial_movie_files)) | ||
return self.partial_movie_files[self.renderer.num_plays] | ||
|
||
def end_animation(self, allow_write=False): | ||
"""Closes the input buffer and streams the rendered partial movie file. | ||
|
||
Called at the end of the life cycle of an :class:`~.Animation`. | ||
|
||
""" | ||
super().end_animation(allow_write=allow_write) | ||
self.stream() | ||
|
||
def combine_movie_files(self): | ||
"""Overridden: not required for live streaming. | ||
|
||
.. seealso:: | ||
|
||
:class:`~.SceneFileWriter` | ||
|
||
""" | ||
pass | ||
|
||
def stream(self): | ||
naveen521kk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Stream the (partial) video file via the configured streaming protocol.""" | ||
logger.info( | ||
"Houston, we are ready to launch. Sending over to %(url)s", | ||
naveen521kk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{"url": {self.streaming_url}}, | ||
) | ||
command = [ | ||
FFMPEG_BIN, | ||
"-re", | ||
"-i", | ||
self.file_path, | ||
"-vcodec", | ||
"copy", | ||
"-an", | ||
"-loglevel", | ||
"quiet", | ||
] | ||
|
||
if self.streaming_protocol == "rtp": | ||
command += ["-sdp_file", self.sdp_path] | ||
command += [ | ||
"-f", | ||
self.streaming_protocol, # Take a look here for other streaming protocols | ||
self.streaming_url, | ||
] | ||
os.system(" ".join(command)) | ||
|
||
def close_movie_pipe(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More or less the same comment as above. Also, I am a bit confused: shouldn't the partial movie files also exist for livestreaming? If so, why is overriding this necessary at all? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Damn I see the flaw! Good sopt |
||
self.writing_process.stdin.close() | ||
self.writing_process.wait() | ||
logger.info( | ||
f"Animation {self.renderer.num_plays}: File at %(path)s", | ||
{"path": {self.file_path}}, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of the click update, this section is no longer being doctested due to popular request, but feel free to update it here anyways.