-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Let SceneFileWriter
access ffmpeg
via av
instead of via external process
#3501
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 14 commits
61222b5
fa18ed5
9ad5275
99e4e2f
de1721b
205ab34
86967dd
fabd4d1
068689b
fb4233b
d8b99e8
0f33f2b
9adb09d
93cdf35
c1afbd6
50f5790
efcb12d
6f7523d
3b7772a
6f62ed1
5f76ccf
11aebee
176e177
ca12971
26be507
0dd41d1
325ccba
5f848bf
788270c
904cfb4
0ec9ab2
8cc54c7
2df10f6
3983e13
1286897
46b79d3
156424d
7c96eaf
eeaa3d9
6ed0362
717c50f
3c2d56b
3ace234
731b287
2e41ce4
10e13e1
d03aadb
16b0408
74941f3
be993f1
7a4e6dc
463a630
e3f1c6f
ead0b58
e22e485
cded948
c147bca
736b1ee
e10e726
4fbbd3c
0ddb1a0
7ed675f
f9aa05f
931caab
23ec3b2
86e78cc
7c716e4
482a108
cd9c4b4
6c0bf90
18f0020
a6f381f
d839aa1
ec799a9
83758b8
bafd285
9d33785
7d10ac7
a8a3228
c4c44f4
2882ed1
f4ae088
7bca93b
f16d739
397c6e0
305fb31
9f01f43
4aec603
85d2326
fb5132d
9ec6169
60abd6e
e7a0f3e
e0a2f29
418ef26
8cb266c
28718aa
f1485e9
ca711b0
4c45836
518bef9
03651f7
25a2419
1d8176d
bb9fb87
38b9011
f50efa4
4de6f85
e60b542
5fe5bb5
6f2473f
174a631
917294f
d8ff9aa
aeaca15
4727a09
761ab71
db8fac0
e0a664d
4633c41
2781a6d
39dcb6e
dd3a545
15abc8c
d91f412
3ddbcc3
f2a6e66
14ccf50
1eb5a42
878debf
7d0c892
0048b72
36207c6
e3f9ced
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 |
---|---|---|
|
@@ -5,12 +5,14 @@ | |
__all__ = ["SceneFileWriter"] | ||
|
||
import json | ||
import logging | ||
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved
Hide resolved
|
||
import os | ||
import shutil | ||
import subprocess | ||
from pathlib import Path | ||
from typing import TYPE_CHECKING, Any | ||
|
||
import av | ||
import numpy as np | ||
import srt | ||
from PIL import Image | ||
|
@@ -24,7 +26,6 @@ | |
from ..utils.file_ops import ( | ||
add_extension_if_not_present, | ||
add_version_before_extension, | ||
ensure_executable, | ||
guarantee_existence, | ||
is_gif_format, | ||
is_png_format, | ||
|
@@ -83,14 +84,6 @@ def __init__(self, renderer, scene_name, **kwargs): | |
self.next_section( | ||
name="autocreated", type=DefaultSectionType.NORMAL, skip_animations=False | ||
) | ||
# fail fast if ffmpeg is not found | ||
if not ensure_executable(Path(config.ffmpeg_executable)): | ||
raise RuntimeError( | ||
"Manim could not find ffmpeg, which is required for generating video output.\n" | ||
"For installing ffmpeg please consult https://docs.manim.community/en/stable/installation.html\n" | ||
"Make sure to either add ffmpeg to the PATH environment variable\n" | ||
"or set path to the ffmpeg executable under the ffmpeg header in Manim's configuration." | ||
) | ||
|
||
def init_output_directories(self, scene_name): | ||
"""Initialise output directories. | ||
|
@@ -358,7 +351,7 @@ def begin_animation(self, allow_write: bool = False, file_path=None): | |
Whether or not to write to a video file. | ||
""" | ||
if write_to_movie() and allow_write: | ||
self.open_movie_pipe(file_path=file_path) | ||
self.open_partial_movie_stream(file_path=file_path) | ||
|
||
def end_animation(self, allow_write: bool = False): | ||
""" | ||
|
@@ -371,7 +364,7 @@ def end_animation(self, allow_write: bool = False): | |
Whether or not to write to a video file. | ||
""" | ||
if write_to_movie() and allow_write: | ||
self.close_movie_pipe() | ||
self.close_partial_movie_stream() | ||
|
||
def write_frame(self, frame_or_renderer: np.ndarray | OpenGLRenderer): | ||
""" | ||
|
@@ -388,15 +381,17 @@ def write_frame(self, frame_or_renderer: np.ndarray | OpenGLRenderer): | |
elif config.renderer == RendererType.CAIRO: | ||
frame = frame_or_renderer | ||
if write_to_movie(): | ||
self.writing_process.stdin.write(frame.tobytes()) | ||
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba") | ||
for packet in self.video_stream.encode(av_frame): | ||
self.video_container.mux(packet) | ||
if is_png_format() and not config["dry_run"]: | ||
self.output_image_from_array(frame) | ||
|
||
def write_opengl_frame(self, renderer: OpenGLRenderer): | ||
if write_to_movie(): | ||
self.writing_process.stdin.write( | ||
renderer.get_raw_frame_buffer_object_data(), | ||
) | ||
av_frame = av.VideoFrame.from_ndarray(renderer.get_frame(), format="rgba") | ||
behackl marked this conversation as resolved.
Show resolved
Hide resolved
behackl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for packet in self.video_stream.encode(av_frame): | ||
self.video_container.mux(packet) | ||
elif is_png_format() and not config["dry_run"]: | ||
target_dir = self.image_file_path.parent / self.image_file_path.stem | ||
extension = self.image_file_path.suffix | ||
|
@@ -467,11 +462,11 @@ def finish(self): | |
if self.subcaptions: | ||
self.write_subcaption_file() | ||
|
||
def open_movie_pipe(self, file_path=None): | ||
""" | ||
Used internally by Manim to initialise | ||
FFMPEG and begin writing to FFMPEG's input | ||
buffer. | ||
def open_partial_movie_stream(self, file_path=None): | ||
"""Open a container holding a video stream. | ||
|
||
This is used internally by Manim initialize the container holding | ||
the video stream of a partial movie file. | ||
""" | ||
if file_path is None: | ||
file_path = self.partial_movie_files[self.renderer.num_plays] | ||
|
@@ -480,49 +475,45 @@ def open_movie_pipe(self, file_path=None): | |
fps = config["frame_rate"] | ||
if fps == int(fps): # fps is integer | ||
fps = int(fps) | ||
if config.renderer == RendererType.OPENGL: | ||
width, height = self.renderer.get_pixel_shape() | ||
else: | ||
height = config["pixel_height"] | ||
width = config["pixel_width"] | ||
|
||
command = [ | ||
config.ffmpeg_executable, | ||
"-y", # overwrite output file if it exists | ||
"-f", | ||
"rawvideo", | ||
"-s", | ||
"%dx%d" % (width, height), # size of one frame | ||
"-pix_fmt", | ||
"rgba", | ||
"-r", | ||
str(fps), # frames per second | ||
"-i", | ||
"-", # The input comes from a pipe | ||
"-an", # Tells FFMPEG not to expect any audio | ||
"-loglevel", | ||
config["ffmpeg_loglevel"].lower(), | ||
"-metadata", | ||
f"comment=Rendered with Manim Community v{__version__}", | ||
] | ||
if config.renderer == RendererType.OPENGL: | ||
command += ["-vf", "vflip"] | ||
|
||
video_container = av.open(file_path, mode="w") | ||
|
||
partial_movie_file_codec = "libx264" | ||
partial_movie_file_pix_fmt = "yuv420p" | ||
av_options = { | ||
"an": "1", # ffmpeg: -an, no audio | ||
} | ||
if is_webm_format(): | ||
command += ["-vcodec", "libvpx-vp9", "-auto-alt-ref", "0"] | ||
# .mov format | ||
elif config["transparent"]: | ||
command += ["-vcodec", "qtrle"] | ||
else: | ||
command += ["-vcodec", "libx264", "-pix_fmt", "yuv420p"] | ||
command += [file_path] | ||
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE) | ||
partial_movie_file_codec = "libvpx-vp9" | ||
partial_movie_file_pix_fmt = "rgba" | ||
av_options["-auto-alt-ref"] = "1" | ||
elif config.transparent: | ||
partial_movie_file_codec = "qtrle" | ||
partial_movie_file_pix_fmt = "rgba" | ||
|
||
stream = video_container.add_stream( | ||
partial_movie_file_codec, | ||
rate=config.frame_rate, | ||
options=av_options, | ||
) | ||
stream.pix_fmt = partial_movie_file_pix_fmt | ||
stream.width = config.pixel_width | ||
stream.height = config.pixel_height | ||
|
||
def close_movie_pipe(self): | ||
""" | ||
Used internally by Manim to gracefully stop writing to FFMPEG's input buffer | ||
self.video_container = video_container | ||
self.video_stream = stream | ||
|
||
def close_partial_movie_stream(self): | ||
"""Close the currently opened video container. | ||
|
||
Used internally by Manim to first flush the remaining packages | ||
in the video stream holding a partial file, and then close | ||
the corresponding container. | ||
""" | ||
self.writing_process.stdin.close() | ||
self.writing_process.wait() | ||
for packet in self.video_stream.encode(): | ||
self.video_container.mux(packet) | ||
|
||
self.video_container.close() | ||
|
||
logger.info( | ||
f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s", | ||
|
@@ -567,37 +558,41 @@ def combine_files( | |
for pf_path in input_files: | ||
pf_path = Path(pf_path).as_posix() | ||
fp.write(f"file 'file:{pf_path}'\n") | ||
commands = [ | ||
config.ffmpeg_executable, | ||
"-y", # overwrite output file if it exists | ||
"-f", | ||
"concat", | ||
"-safe", | ||
"0", | ||
"-i", | ||
str(file_list), | ||
"-loglevel", | ||
config.ffmpeg_loglevel.lower(), | ||
"-metadata", | ||
f"comment=Rendered with Manim Community v{__version__}", | ||
"-nostdin", | ||
] | ||
|
||
av_options = { | ||
"safe": "0", | ||
"metadata": f"comment=Rendered with Manim Community v{__version__}", | ||
} | ||
if create_gif: | ||
commands += [ | ||
"-vf", | ||
f"fps={np.clip(config['frame_rate'], 1, 50)},split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle", | ||
] | ||
else: | ||
commands += ["-c", "copy"] | ||
av_options["vf"] = ( # add video filter (is there a better way to do this?) | ||
f"fps={np.clip(config['frame_rate'], 1, 50)}," | ||
"split[s0][s1];[s0]palettegen=stats_mode=diff[p];" | ||
"[s1][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" | ||
) | ||
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. Seems like video filters must be applied separately: PyAV-Org/PyAV#239 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. Yes, I've tried the Well, seems like we'll have to look into it regardless. 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. But does the |
||
else: # copy codec of combined files | ||
av_options["c"] = "copy" | ||
|
||
if not includes_sound: | ||
commands += ["-an"] | ||
av_options["an"] = "1" | ||
|
||
partial_movies_input = av.open( | ||
JasonGrace2282 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
str(file_list), options=av_options, format="concat" | ||
) | ||
partial_movies_stream = partial_movies_input.streams.video[0] | ||
output_container = av.open(str(output_file), mode="w") | ||
output_stream = output_container.add_stream(template=partial_movies_stream) | ||
|
||
for packet in partial_movies_input.demux(partial_movies_stream): | ||
# We need to skip the "flushing" packets that `demux` generates. | ||
if packet.dts is None: | ||
continue | ||
|
||
commands += [str(output_file)] | ||
# We need to assign the packet to the new stream. | ||
packet.stream = output_stream | ||
output_container.mux(packet) | ||
|
||
combine_process = subprocess.Popen(commands) | ||
combine_process.wait() | ||
partial_movies_input.close() | ||
output_container.close() | ||
|
||
def combine_to_movie(self): | ||
"""Used internally by Manim to combine the separate | ||
|
Uh oh!
There was an error while loading. Please reload this page.