plesty.lib.ui.qt.recorder ========================= .. py:module:: plesty.lib.ui.qt.recorder .. autoapi-nested-parse:: Recording a shell: one captured frame per refresh, encoded on stop. A measurement that took two hours is shown to a colleague, a supervisor, or a paper reviewer as a video of the run building up — the same arrangement of panels the operator watched, at whatever speed makes it readable. Recording therefore belongs to the window, not to any one view: whatever is docked gets captured, including panels added halfway through. Capture is a window grab on the GUI thread, which is cheap and, more importantly, needs no cooperation from the panels. Frames are size-locked to the first capture, so resizing or floating a panel mid-recording cannot corrupt the stream — the resized window is letterboxed into the frame it started with. MP4 is written through ``imageio`` (with ``imageio-ffmpeg``) when both are installed, and an animated GIF through Pillow otherwise, so a recording is always produced:: uv add "plesty-lib[record]" Attributes ---------- .. autoapisummary:: plesty.lib.ui.qt.recorder.DEFAULT_FPS Classes ------- .. autoapisummary:: plesty.lib.ui.qt.recorder.Recorder Functions --------- .. autoapisummary:: plesty.lib.ui.qt.recorder._to_array plesty.lib.ui.qt.recorder._fit Module Contents --------------- .. py:data:: DEFAULT_FPS :value: 10 .. py:function:: _to_array(widget: PySide6.QtWidgets.QWidget) -> numpy.ndarray Grab *widget* as an RGB array. :param widget: The window (or panel) to capture. :returns: The pixels as a ``(height, width, 3)`` uint8 array. .. py:function:: _fit(frame: numpy.ndarray, shape: tuple[int, int]) -> numpy.ndarray Letterbox *frame* into *shape*, so every frame of a video matches. .. py:class:: Recorder(widget: PySide6.QtWidgets.QWidget, path: str | pathlib.Path, *, fps: int = DEFAULT_FPS, every: int = 1) Capture a window while active; encode a video file on stop. .. code-block:: python shell.start_recording("run.mp4", fps=10) ... # the shell captures on every tick path = shell.stop_recording() # → run.mp4, or run.gif without ffmpeg Prepare a recording of *widget*. :param widget: The window to capture — normally the shell itself. :param path: Destination file; the suffix is corrected to what the available encoder can actually write. :param fps: Frame rate of the encoded video. :param every: Capture one frame per *every* refreshes, for a run whose rows arrive faster than a video needs them. :raises ValueError: If *fps* or *every* is not positive. .. py:attribute:: widget .. py:attribute:: path .. py:attribute:: fps :value: 10 .. py:attribute:: every :value: 1 .. py:attribute:: active :value: False .. py:attribute:: frames :type: list[numpy.ndarray] :value: [] .. py:attribute:: _shape :type: Optional[tuple[int, int]] :value: None .. py:attribute:: _seen :value: 0 .. py:method:: start() -> None Begin capturing; a restart discards what was captured before. .. py:method:: capture() -> None Capture one frame, honouring :attr:`every`; ignored when inactive. .. py:method:: stop() -> Optional[pathlib.Path] Stop capturing and encode what was captured. :returns: The written file, or ``None`` when nothing was captured — a recording that never saw a frame writes no file rather than an empty one. .. py:method:: _encode() -> pathlib.Path Write the captured frames, preferring MP4 and falling back to GIF. .. py:method:: _encode_gif() -> pathlib.Path Write an animated GIF, the fallback that needs only Pillow. .. py:method:: describe() -> dict[str, Any] Return what has been captured so far. :returns: Whether the recorder is active, the frame count, and the target.