mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-08-06 06:12:56 +08:00
chore(deps): remove subprocess calls to FFMPEG with av
(#335)
* chore(deps): remove subprocess calls to FFMPEG with `av` Linked to the progess made in https://github.com/ManimCommunity/manim/pull/3501. The following PR aims at reducing subprocess calls for security and speed reasons. Also, with https://github.com/ManimCommunity/manim/pull/3501 is merged, FFMPEG should not be needed anymore as it is part of `PyAv`. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(lib): oops forgot to commit those changes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore(lib): clear assets and clean code * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore(doc): document changes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -2,4 +2,3 @@ from pathlib import Path
|
||||
|
||||
FOLDER_PATH: Path = Path("./slides")
|
||||
CONFIG_PATH: Path = Path(".manim-slides.toml")
|
||||
FFMPEG_BIN: Path = Path("ffmpeg")
|
||||
|
@ -41,6 +41,9 @@ def make_logger() -> logging.Logger:
|
||||
logger.setLevel(logging.getLogger("manim").level)
|
||||
logger.addHandler(rich_handler)
|
||||
|
||||
if not (libav_logger := logging.getLogger("libav")).hasHandlers():
|
||||
libav_logger.addHandler(rich_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@ import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
|
||||
from ..defaults import FFMPEG_BIN, FOLDER_PATH
|
||||
from ..defaults import FOLDER_PATH
|
||||
from ..logger import logger
|
||||
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
|
||||
from . import MANIM
|
||||
@ -47,11 +47,6 @@ class BaseSlide:
|
||||
self._canvas: MutableMapping[str, Mobject] = {}
|
||||
self._wait_time_between_slides = 0.0
|
||||
|
||||
@property
|
||||
def _ffmpeg_bin(self) -> Path:
|
||||
"""Return the path to the ffmpeg binaries."""
|
||||
return FFMPEG_BIN
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _frame_height(self) -> float:
|
||||
@ -492,11 +487,11 @@ class BaseSlide:
|
||||
|
||||
# We only concat animations if it was not present
|
||||
if not use_cache or not dst_file.exists():
|
||||
concatenate_video_files(self._ffmpeg_bin, slide_files, dst_file)
|
||||
concatenate_video_files(slide_files, dst_file)
|
||||
|
||||
# We only reverse video if it was not present
|
||||
if not use_cache or not rev_file.exists():
|
||||
reverse_video_file(self._ffmpeg_bin, dst_file, rev_file)
|
||||
reverse_video_file(dst_file, rev_file)
|
||||
|
||||
slides.append(
|
||||
SlideConfig.from_pre_slide_config_and_files(
|
||||
|
@ -13,15 +13,6 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
for slides rendering.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _ffmpeg_bin(self) -> Path:
|
||||
# Prior to v0.16.0.post0,
|
||||
# ffmpeg was stored as a constant in manim.constants
|
||||
try:
|
||||
return Path(config.ffmpeg_executable)
|
||||
except AttributeError:
|
||||
return super()._ffmpeg_bin
|
||||
|
||||
@property
|
||||
def _frame_height(self) -> float:
|
||||
return config["frame_height"] # type: ignore
|
||||
|
@ -1,45 +1,37 @@
|
||||
import hashlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import av
|
||||
|
||||
from .logger import logger
|
||||
|
||||
|
||||
def concatenate_video_files(ffmpeg_bin: Path, files: List[Path], dest: Path) -> None:
|
||||
def concatenate_video_files(files: List[Path], dest: Path) -> None:
|
||||
"""Concatenate multiple video files into one."""
|
||||
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
||||
f.writelines(f"file '{path.absolute()}'\n" for path in files)
|
||||
f.close()
|
||||
|
||||
command: List[str] = [
|
||||
str(ffmpeg_bin),
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
f.name,
|
||||
"-c",
|
||||
"copy",
|
||||
str(dest),
|
||||
"-y",
|
||||
]
|
||||
logger.debug(" ".join(command))
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output, error = process.communicate()
|
||||
input_ = av.open(f.name, options={"safe": "0"}, format="concat")
|
||||
input_stream = input_.streams.video[0]
|
||||
output = av.open(str(dest), mode="w")
|
||||
output_stream = output.add_stream(
|
||||
template=input_stream,
|
||||
)
|
||||
|
||||
if output:
|
||||
logger.debug(output.decode())
|
||||
for packet in input_.demux(input_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
if error:
|
||||
logger.debug(error.decode())
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_stream
|
||||
output.mux(packet)
|
||||
|
||||
if not dest.exists():
|
||||
raise ValueError(
|
||||
"could not properly concatenate files, use `-v DEBUG` for more details"
|
||||
)
|
||||
input_.close()
|
||||
output.close()
|
||||
|
||||
|
||||
def merge_basenames(files: List[Path]) -> Path:
|
||||
@ -63,15 +55,44 @@ def merge_basenames(files: List[Path]) -> Path:
|
||||
return dirname.joinpath(basename + ext)
|
||||
|
||||
|
||||
def reverse_video_file(ffmpeg_bin: Path, src: Path, dst: Path) -> None:
|
||||
"""Reverses a video file, writting the result to `dst`."""
|
||||
command = [str(ffmpeg_bin), "-y", "-i", str(src), "-vf", "reverse", str(dst)]
|
||||
logger.debug(" ".join(command))
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output, error = process.communicate()
|
||||
def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
|
||||
"""Code from https://github.com/PyAV-Org/PyAV/issues/239."""
|
||||
for c, n in zip(nodes, nodes[1:]):
|
||||
c.link_to(n)
|
||||
|
||||
if output:
|
||||
logger.debug(output.decode())
|
||||
|
||||
if error:
|
||||
logger.debug(error.decode())
|
||||
def reverse_video_file(src: Path, dest: Path) -> None:
|
||||
"""Reverses a video file, writting the result to `dest`."""
|
||||
input_ = av.open(str(src))
|
||||
input_stream = input_.streams.video[0]
|
||||
output = av.open(str(dest), mode="w")
|
||||
output_stream = output.add_stream(codec_name="libx264", rate=input_stream.base_rate)
|
||||
output_stream.width = input_stream.width
|
||||
output_stream.height = input_stream.height
|
||||
output_stream.pix_fmt = input_stream.pix_fmt
|
||||
|
||||
graph = av.filter.Graph()
|
||||
link_nodes(
|
||||
graph.add_buffer(template=input_stream),
|
||||
graph.add("reverse"),
|
||||
graph.add("buffersink"),
|
||||
)
|
||||
graph.configure()
|
||||
|
||||
frames_count = 0
|
||||
for frame in input_.decode(video=0):
|
||||
graph.push(frame)
|
||||
frames_count += 1
|
||||
|
||||
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
|
||||
|
||||
for i in range(frames_count):
|
||||
frame = graph.pull()
|
||||
frame.pict_type = 1 # Otherwise we get a warning saying it is changed
|
||||
output.mux(output_stream.encode(frame))
|
||||
|
||||
for packet in output_stream.encode():
|
||||
output.mux(packet)
|
||||
|
||||
input_.close()
|
||||
output.close()
|
||||
|
Reference in New Issue
Block a user