def run_window_config(config_cls: WindowConfig, timer=None, args=None) -> None: """ Run an WindowConfig entering a blocking main loop Args: config_cls: The WindowConfig class to render args: Override sys.args """ setup_basic_logging(config_cls.log_level) parser = create_parser() config_cls.add_arguments(parser) values = parse_args(args=args, parser=parser) config_cls.argv = values window_cls = get_local_window_cls(values.window) # Calculate window size size = values.size or config_cls.window_size size = int(size[0] * values.size_mult), int(size[1] * values.size_mult) # Resolve cursor show_cursor = values.cursor if show_cursor is None: show_cursor = config_cls.cursor window = window_cls( title=config_cls.title, size=size, fullscreen=config_cls.fullscreen or values.fullscreen, resizable=values.resizable if values.resizable is not None else config_cls.resizable, gl_version=config_cls.gl_version, aspect_ratio=config_cls.aspect_ratio, vsync=values.vsync if values.vsync is not None else config_cls.vsync, samples=values.samples if values.samples is not None else config_cls.samples, cursor=show_cursor if show_cursor is not None else True, ) window.print_context_info() activate_context(window=window) timer = Timer() window.config = config_cls(ctx=window.ctx, wnd=window, timer=timer) timer.start() while not window.is_closing: current_time, delta = timer.next_frame() if window.config.clear_color is not None: window.clear(*window.config.clear_color) else: window.use() window.render(current_time, delta) window.swap_buffers() _, duration = timer.stop() window.destroy() if duration > 0: logger.info("Duration: {0:.2f}s @ {1:.2f} FPS".format( duration, window.frames / duration))
def run_window_config(config_cls: WindowConfig, timer=None, args=None) -> None: """ Run an WindowConfig entering a blocking main loop Args: config_cls: The WindowConfig class to render args: Override sys.args """ values = parse_args(args) window_cls = get_local_window_cls(values.window) window = window_cls( title=config_cls.title, size=config_cls.window_size, fullscreen=values.fullscreen, resizable=config_cls.resizable, gl_version=config_cls.gl_version, aspect_ratio=config_cls.aspect_ratio, vsync=values.vsync, samples=values.samples, cursor=values.cursor, ) window.print_context_info() window.config = config_cls(ctx=window.ctx, wnd=window) timer = Timer() timer.start() while not window.is_closing: current_time, delta = timer.next_frame() window.ctx.screen.use() window.render(current_time, delta) window.swap_buffers() _, duration = timer.stop() window.destroy() print("Duration: {0:.2f}s @ {1:.2f} FPS".format(duration, window.frames / duration))
class VideoCapture: """ ``VideoCapture`` it's an utility class to capture runtime render and save it as video. Example: .. code:: python import moderngl_window from moderngl_window.capture import VideoCapture class CaptureTest(modenrgl_window.WindowConfig): def __init__(self, **kwargs): super().__init__(**kwargs) # do other initialization # define VideoCapture class self.cap = VideoCapture() # start recording self.cap.start_capture( filename="video.mp4", target_fb = self.wnd.fbo ) def render(self, time, frametime): # do other render stuff # call record function after self.cap.dump() def close(self): # if realease func is not called during # runtime. make sure to do it on the closing event self.cap.release() """ def __init__(self): self._ffmpeg = None self._video_timer = Timer() self._filename: str = None self._target_fb: moderngl.Framebuffer = None self._framerate: int = None self._last_frame = None self._recording = False @property def framerate(self) -> int: return self._framerate @framerate.setter def framerate(self, value: int): self._framerate = value @property def target_fb(self) -> moderngl.Framebuffer: return self._target_fb @target_fb.setter def target_fb(self, value: moderngl.Framebuffer): self._target_fb = value @property def filename(self) -> str: return self._filename @filename.setter def filename(self, value: str): self._filename = value def dump(self): """ Read data from the target framebuffer and dump the raw data into ffmpeg stdin. Call this function at the end of `render` function Frame are saved respecting the video framerate. """ if not self._recording: return # in theory to capture a frame at certain speed i'm testing if # the time passed after the last frame is at least dt = 1./target_fps . # This prevent the higher framerate during runtime to exceed the # target framerate of the video. This doesn't work if runtime framerate # is lower than target framerate. if (self._video_timer.time - self._last_frame) >= 1./self._framerate: data = self._target_fb.read(components=3) self._ffmpeg.stdin.write(data) self._last_frame = self._video_timer.time def start(self, filename: str = None, target_fb: moderngl.Framebuffer = None, framerate=60): """ Start ffmpeg pipe subprocess. Call this at the end of __init__ function. Args: filename (str): name of the output file fb (moderngl.Framebuffer): target framebuffer to record framerate (int): framerate of the video """ if not target_fb: raise Exception("target framebuffer can't be: None") else: self._target_fb = target_fb self._framerate = framerate if not filename: now = datetime.datetmie.now() filename = f'video_{now:%Y-%m-%d_%H:%M:%S.%f}.mp4' self._filename = filename width = target_fb.width height = target_fb.height # took from Wasaby2D project command = [ 'ffmpeg', '-y', # (optional) overwrite output file if it exists '-f', 'rawvideo', '-vcodec', 'rawvideo', '-s', f'{width}x{height}', # size of one frame '-pix_fmt', 'rgb24', '-r', f'{framerate}', # frames per second '-i', '-', # The imput comes from a pipe '-vf', 'vflip', '-an', # Tells FFMPEG not to expect any audio filename, ] # ffmpeg binary need to be on the PATH. try: self._ffmpeg = subprocess.Popen( command, stdin=subprocess.PIPE, bufsize=0 ) except FileNotFoundError: print("ffmpeg command not found.") return self._video_timer.start() self._last_frame = self._video_timer.time self._recording = True print("Started video Recording") def release(self): """ Stop the recording process """ if self._recording: self._ffmpeg.stdin.close() ret = self._ffmpeg.wait() if ret == 0: print("Video saved succesfully") else: print("Error writing video.") self._recording = None self._video_timer.stop()
class BaseVideoCapture: """ ``BaseVideoCapture`` is a base class to video capture Args: source (moderngl.Texture, moderngl.Framebuffer): the source of the capture framerate (int, float) : the framerate of the video, by thefault is 60 fps if the source is texture there are some requirements: - dtype = 'f1'; - components >= 3. """ def __init__( self, source: Union[moderngl.Texture, moderngl.Framebuffer] = None, framerate: Union[int, float] = 60, ): self._source = source self._framerate = framerate self._recording = False self._last_time: float = None self._filename: str = None self._width: int = None self._height: int = None self._timer = Timer() self._components: int = None # for textures if isinstance(self._source, moderngl.Texture): self._components = self._source.components def _dump_frame(self, frame): """ custom function called during self.save() Args: frame: frame data in bytes """ raise NotImplementedError("override this function") def _start_func(self) -> bool: """ custom function called during self.start_capture() must return a True if this function complete without errors """ raise NotImplementedError("override this function") def _release_func(self): """ custom function called during self.realease() """ raise NotImplementedError("override this function") def _get_wh(self): """ Return a tuple of the width and the height of the source """ return self._source.width, self._source.height def _remove_file(self): """ Remove the filename of the video is it exist """ if os.path.exists(self._filename): os.remove(self._filename) def start_capture(self, filename: str = None, framerate: Union[int, float] = 60): """ Start the capturing process Args: filename (str): name of the output file framerate (int, float): framerate of the video if filename is not specified it will be generated based on the datetime. """ if self._recording: print("Capturing is already started") return # ensure the texture has the correct dtype and components if isinstance(self._source, moderngl.Texture): if self._source.dtype != 'f1': print("source type: moderngl.Texture must be type `f1` ") return if self._components < 3: print( "source type: moderngl.Texture must have at least 3 components" ) return if not filename: now = datetime.datetime.now() filename = f'video_{now:%Y%m%d_%H%M%S}.mp4' self._filename = filename self._framerate = framerate self._width, self._height = self._get_wh() # if something goes wrong with the start # function, just stop and release the # capturing process if not self._start_func(): self.release() print("Capturing failed") return self._timer.start() self._last_time = self._timer.time self._recording = True def save(self): """ Save function to call at the end of render function """ if not self._recording: return dt = 1. / self._framerate if self._timer.time - self._last_time > dt: # start counting self._last_time = self._timer.time if isinstance(self._source, moderngl.Framebuffer): # get data from framebuffer frame = self._source.read(components=3) self._dump_frame(frame) else: # get data from texture frame = self._source.read() self._dump_frame(frame) def release(self): """ Stop the recording process """ if self._recording: self._release_func() self._timer.stop() print(f"Video file succesfully saved as {self._filename}") self._recording = None