Skip to content
Snippets Groups Projects
Verified Commit 00d49db5 authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

chore: moved video code out of main gb file

Also made extension (container) and video encoder separate concepts.
More modularity to the way encoding is performed.
parent de537427
No related branches found
No related tags found
1 merge request!36Support for Python
Pipeline #3629 passed
from .gb import (
from .gb import GameBoyMode, GameBoy
from .palettes import PALETTES
from .video import VideoCapture
from .boytacean import (
DISPLAY_WIDTH,
DISPLAY_HEIGHT,
CPU_FREQ,
VISUAL_FREQ,
LCD_CYCLES,
GameBoy,
GameBoyMode,
GameBoyRust,
GameBoy as GameBoyRust,
)
from .palettes import PALETTES
from enum import Enum
from glob import glob
from math import ceil
from shutil import rmtree
from tempfile import mkdtemp
from contextlib import contextmanager
from typing import Any, Iterable, Union
from PIL.Image import Image, frombytes
from .palettes import PALETTES
from .video import VideoCapture
from .boytacean import (
DISPLAY_WIDTH,
DISPLAY_HEIGHT,
CPU_FREQ,
VISUAL_FREQ,
LCD_CYCLES,
GameBoy as GameBoyRust,
)
......@@ -28,9 +22,7 @@ class GameBoyMode(Enum):
class GameBoy:
_frame_index: int = 0
_start_frame: Union[int, None]
_frame_gap: int
_capture_temp_dir: Union[str, None]
_video: Union[VideoCapture, None] = None
def __init__(
self,
......@@ -44,9 +36,7 @@ class GameBoy:
):
super().__init__()
self._frame_index = 0
self._next_frame = None
self._frame_gap = VISUAL_FREQ
self._capture_temp_dir = None
self._video = None
self._system = GameBoyRust(mode.value)
self._system.set_ppu_enabled(ppu_enabled)
self._system.set_apu_enabled(apu_enabled)
......@@ -99,42 +89,22 @@ This is a [Game Boy](https://en.wikipedia.org/wiki/Game_Boy) emulator built usin
image = frombytes("RGB", (DISPLAY_WIDTH, DISPLAY_HEIGHT), frame_buffer, "raw")
return image
def save_image(self, filename: str, format: str = "PNG"):
def save_image(self, filename: str, format: str = "png"):
image = self.image()
image.save(filename, format=format)
image.save(f"{filename}.{format.lower()}", format=format)
def video(
self,
encoder="avc1",
display=True,
file_name="output.mp4",
frame_glob="frame_*.png",
) -> Any:
from cv2 import VideoWriter, VideoWriter_fourcc, imread
from IPython.display import Video, display as _display
from IPython.display import display as _display
image_paths = glob(f"{self._capture_temp_dir}/{frame_glob}")
video_path = f"{self._capture_temp_dir}/{file_name}"
encoder = VideoWriter(
video_path,
VideoWriter_fourcc(*encoder),
VISUAL_FREQ / self._frame_gap,
(DISPLAY_WIDTH, DISPLAY_HEIGHT),
)
try:
for image_file in sorted(image_paths):
image = imread(image_file)
encoder.write(image)
finally:
encoder.release()
video = Video(video_path, embed=True, html_attributes="controls loop autoplay")
if self._video == None:
raise RuntimeError("Not capturing a video")
video = self._video.build()
if display:
_display(video)
return video
def set_palette(self, name: str):
......@@ -198,27 +168,54 @@ This is a [Game Boy](https://en.wikipedia.org/wiki/Game_Boy) emulator built usin
return PALETTES.keys()
@contextmanager
def video_capture(self, fps=5):
self._start_capture(fps=fps)
def video_capture(
self,
video_format="avc1",
video_extension="mp4",
video_name="output",
fps=5,
frame_format="png",
):
self._start_capture(
video_format=video_format,
video_extension=video_extension,
video_name=video_name,
fps=fps,
frame_format=frame_format,
)
try:
yield
finally:
self.video()
self._stop_capture()
try:
self.video()
finally:
self._stop_capture()
def _on_next_frame(self):
if self._next_frame != None and self._frame_index >= self._next_frame:
self._next_frame = self._next_frame + self._frame_gap
self.save_image(
f"{self._capture_temp_dir}/frame_{self._frame_index:08d}.png"
)
if self._video != None and self._video.should_capture(self._frame_index):
self._video.save_frame(self.image(), self._frame_index)
self._video.compute_next(self._frame_index)
def _start_capture(self, fps=5):
self._next_frame = self._frame_index + self._frame_gap
self._frame_gap = ceil(VISUAL_FREQ / fps)
self._capture_temp_dir = mkdtemp()
def _start_capture(
self,
video_format="avc1",
video_extension="mp4",
video_name="output",
fps=5,
frame_format="png",
):
if self._video != None:
raise RuntimeError("Already capturing a video")
self._video = VideoCapture(
start_frame=self._frame_index,
video_format=video_format,
video_extension=video_extension,
video_name=video_name,
fps=fps,
frame_format=frame_format,
)
def _stop_capture(self):
self._next_frame = None
if self._capture_temp_dir:
rmtree(self._capture_temp_dir)
if self._video:
self._video.cleanup()
self._video = None
from glob import glob
from math import ceil
from shutil import rmtree
from typing import Any, Sequence, Union
from tempfile import mkdtemp
from PIL.Image import Image
from .boytacean import (
DISPLAY_WIDTH,
DISPLAY_HEIGHT,
VISUAL_FREQ,
)
FORMATS = {
"mp4": ["avc1", "mp4", "h264", "hev1"],
"webm": ["vp8", "vp9"],
"mkv": ["avc1", "mp4", "h264", "hev1"],
}
class VideoCapture:
_start_frame: Union[int, None] = None
_next_frame: Union[int, None] = None
_video_format: str = "avc1"
_video_extension: str = "mp4"
_video_name: str = "output"
_frame_gap: int = 60
_frame_format: str = "png"
_frame_prefix: str = "frame_"
_capture_temp_dir: Union[str, None] = None
def __init__(
self,
start_frame=0,
video_format="avc1",
video_extension="mp4",
video_name="output",
fps=5,
frame_format="png",
):
super().__init__()
self._start_frame = start_frame
self._video_format = video_format
self._video_extension = video_extension
self._video_name = video_name
self._frame_format = frame_format
self._frame_gap = ceil(VISUAL_FREQ / fps)
self._next_frame = start_frame + self._frame_gap
self._capture_temp_dir = mkdtemp()
@classmethod
def formats(cls, extension="mp4") -> Sequence[str]:
return FORMATS.get(extension, [])
def should_capture(self, frame_index) -> bool:
return self._start_frame == None or frame_index >= self._next_frame
def compute_next(self, frame_index):
self._next_frame = frame_index + self._frame_gap
def frame_path(self, frame_index) -> str:
return f"{self._capture_temp_dir}/{self._frame_prefix}{frame_index:08d}.{self.frame_format_l}"
def cleanup(self):
if self._capture_temp_dir:
rmtree(self._capture_temp_dir)
def save_frame(self, frame: Image, frame_index: int):
frame.save(self.frame_path(frame_index), format=self.frame_format)
def build(self) -> Any:
from cv2 import VideoWriter, VideoWriter_fourcc, imread
from IPython.display import Video
if not self._capture_temp_dir:
raise RuntimeError("Not capturing a video")
image_paths = glob(f"{self.frames_glob}")
video_path = f"{self._capture_temp_dir}/{self.video_filename}"
encoder = VideoWriter(
video_path,
VideoWriter_fourcc(*self.video_format_fourcc),
self.fps,
(DISPLAY_WIDTH, DISPLAY_HEIGHT),
)
try:
for image_file in sorted(image_paths):
image = imread(image_file)
encoder.write(image)
finally:
encoder.release()
return Video(video_path, embed=True, html_attributes="controls loop autoplay")
@property
def fps(self) -> float:
return VISUAL_FREQ / self._frame_gap
@property
def video_filename(self) -> str:
return f"{self._video_name}.{self._video_extension}"
@property
def video_format_l(self) -> str:
return self._video_format.lower()
@property
def video_format_fourcc(self) -> str:
return self.video_format_l.ljust(4, "0")
@property
def frame_format(self) -> str:
return self._frame_format
@property
def frame_format_l(self) -> str:
return self._frame_format.lower()
@property
def frames_glob(self) -> str:
return f"{self._capture_temp_dir}/{self._frame_prefix}*.{self.frame_format_l}"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment