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:
Jérome Eertmans
2023-12-11 13:53:58 +01:00
committed by GitHub
parent 20b7ef4110
commit 74ddefe519
16 changed files with 128 additions and 92 deletions

View File

@ -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")

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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()