Source code for mini_arcade_core.runtime.capture.video_encoder
"""
Video encoding utilities.
"""
from __future__ import annotations
import subprocess
import traceback
from dataclasses import dataclass
from pathlib import Path
from mini_arcade_core.utils import logger
[docs]
@dataclass(frozen=True)
class EncodeResult:
"""
Result of an encoding operation.
:ivar ok (bool): Whether the encoding was successful.
:ivar output_path (Path | None): Path to the encoded video file if successful.
:ivar error (str | None): Error message if the encoding failed.
"""
ok: bool
output_path: Path | None = None
error: str | None = None
# pylint: disable=too-many-arguments
[docs]
def encode_png_sequence_to_mp4(
*,
ffmpeg_path: str,
frames_dir: Path,
output_path: Path,
input_fps: int, # <-- capture_fps
output_fps: (
int | None
) = None, # <-- optional container fps (e.g. 60)video_interpolate: bool = False
video_interpolate: bool = False,
pattern: str = "frame_%08d.png",
codec: str = "libx264",
crf: int = 18,
preset: str = "veryfast",
) -> EncodeResult:
"""
Encodes frames_dir/frame_%08d.png into output_path (mp4).
Assumes contiguous numbering starting at 0.
:param ffmpeg_path: Path to the ffmpeg executable.
:type ffmpeg_path: str
:param frames_dir: Directory containing the PNG frames to encode.
:type frames_dir: Path
:param output_path: Destination path for the encoded video file.
:type output_path: Path
:param input_fps: Frames per second of the input PNG sequence.
:type input_fps: int
:param output_fps: Frames per second for the output video file.
:type output_fps: int | None
:param video_interpolate: Whether to use motion interpolation for output fps.
:type video_interpolate: bool
:param pattern: Filename pattern for input frames.
:type pattern: str
:param codec: Video codec to use for encoding.
:type codec: str
:param crf: Constant Rate Factor for video quality.
:type crf: int
:param preset: Preset for video encoding speed/quality tradeoff.
:type preset: str
:return: Result of the encoding operation.
:rtype: EncodeResult
"""
frames_glob = frames_dir / pattern
output_path.parent.mkdir(parents=True, exist_ok=True)
cmd = [
ffmpeg_path,
"-y",
"-framerate",
str(input_fps), # <-- IMPORTANT
"-i",
str(frames_glob),
]
if output_fps is not None and output_fps > 0:
if video_interpolate:
cmd += [
"-vf",
f"minterpolate=fps={output_fps}:mi_mode=mci:mc_mode=aobmc:vsbmc=1",
]
cmd += ["-r", str(output_fps)]
cmd += [
"-c:v",
codec,
"-pix_fmt",
"yuv420p",
"-crf",
str(crf),
"-preset",
preset,
str(output_path),
]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
if proc.returncode != 0:
return EncodeResult(
ok=False,
error=(proc.stderr or proc.stdout or "ffmpeg failed").strip(),
)
return EncodeResult(ok=True, output_path=output_path)
except FileNotFoundError as exc:
logger.error(f"ffmpeg not found: {exc}")
return EncodeResult(ok=False, error=f"ffmpeg not found: {exc}")
except Exception as exc: # pylint: disable=broad-exception-caught
tb_str = traceback.format_exc()
logger.error(f"ffmpeg encoding error: {exc}\n{tb_str}")
return EncodeResult(ok=False, error=f"ffmpeg encoding error: {exc}")