Skip to content

Commit

Permalink
chore(deps): remove subprocess calls to FFMPEG with av
Browse files Browse the repository at this point in the history
Linked to the progess made in ManimCommunity/manim#3501.

The following PR aims at reducing subprocess calls for security and speed reasons. Also, with ManimCommunity/manim#3501 is merged, FFMPEG should not be needed anymore as it is part of `PyAv`.
  • Loading branch information
jeertmans committed Dec 11, 2023
1 parent d292534 commit 161c3ec
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 63 deletions.
1 change: 1 addition & 0 deletions manim_slides/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def make_logger() -> logging.Logger:
logger = logging.getLogger("manim-slides")
logger.setLevel(logging.getLogger("manim").level)
logger.addHandler(rich_handler)
logging.getLogger("libav").addHandler(rich_handler)

return logger

Expand Down
107 changes: 66 additions & 41 deletions manim_slides/utils.py
Original file line number Diff line number Diff line change
@@ -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(_: Path, 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()

if output:
logger.debug(output.decode())

if error:
logger.debug(error.decode())

if not dest.exists():
raise ValueError(
"could not properly concatenate files, use `-v DEBUG` for more details"
)
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,
)

for packet in input_.demux(input_stream):
# We need to skip the "flushing" packets that `demux` generates.
if packet.dts is None:
continue

# We need to assign the packet to the new stream.
packet.stream = output_stream
output.mux(packet)

input_.close()
output.close()


def merge_basenames(files: List[Path]) -> Path:
Expand All @@ -63,15 +55,48 @@ 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()

if output:
logger.debug(output.decode())

if error:
logger.debug(error.decode())
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)


def reverse_video_file(_: Path, 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=input_stream.codec_context.name,
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 packet in input_.demux(input_stream):
for frame in packet.decode():
graph.push(frame)
frames_count += 1

graph.push(None)

for i in range(frames_count):
frame = graph.pull()
for packet in output_stream.encode(frame):
output.mux(packet)

for packet in output_stream.encode():
output.mux(packet)

input_.close()
output.close()
Loading

0 comments on commit 161c3ec

Please sign in to comment.