Source code for mini_arcade_core.runtime.capture.video

"""
Video recording management.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from uuid import uuid4


[docs] @dataclass class VideoRecordConfig: """ Configuration for video recording. :ivar fps (int): Desired output frames per second. :ivar capture_fps (int): Actual capture frames per second to reduce stalls. :ivar ext (str): File extension for saved frames. :ivar frames_dir (str): Directory to save recorded frames. :ivar prefix (str): Prefix for recording session directories. """ fps: int = 60 # desired output fps in manifest capture_fps: int = 15 # actual capture rate to reduce stalls ext: str = "png" frames_dir: str = "recordings" prefix: str = "run_"
[docs] class VideoRecorder: """Video recording management.""" def __init__(self, cfg: VideoRecordConfig | None = None): """ :param cfg: Configuration for video recording. :type cfg: VideoRecordConfig | None """ self.cfg = cfg or VideoRecordConfig() self.active: bool = False self.run_id: str = "" self.base_dir: Path | None = None self._frame_index: int = 0 self._every_n: int = 1
[docs] def start(self, *, out_dir: Path | None = None) -> Path: """ Start video recording. :param out_dir: Optional output directory for recorded frames. :type out_dir: Path | None :return: Path to the directory where video frames are saved. :rtype: Path """ if self.active: raise RuntimeError("VideoRecorder already active") self.active = True self.run_id = uuid4().hex self.base_dir = ( out_dir or Path(self.cfg.frames_dir) ) / f"{self.cfg.prefix}{self.run_id}" self.base_dir.mkdir(parents=True, exist_ok=True) # reduce load by capturing less frequently # example: game 60fps, capture_fps 15 => every 4 frames self._every_n = max( 1, int(round(self.cfg.fps / max(1, self.cfg.capture_fps))) ) self._frame_index = 0 return self.base_dir
[docs] def stop(self) -> None: """Stop video recording.""" self.active = False self.run_id = "" self.base_dir = None self._frame_index = 0
[docs] def should_capture(self, frame_index: int) -> bool: """ Check if the current frame index should be captured based on the capture frequency. :param frame_index: The current frame index. :type frame_index: int :return: True if the frame should be captured, False otherwise. :rtype: bool """ return self.active and (frame_index % self._every_n == 0)
[docs] def next_paths(self) -> tuple[Path, Path, int]: """ Returns: (tmp_bmp_path, out_png_path, out_frame_index) out_frame_index increases only when we actually capture. :param frame_index: The current frame index. :type frame_index: int :raise: RuntimeError: If the recorder is not active. """ if not self.active or self.base_dir is None: raise RuntimeError("VideoRecorder is not active") out_frame = self._frame_index self._frame_index += 1 out_png = self.base_dir / f"frame_{out_frame:08d}.{self.cfg.ext}" return out_png, out_frame