Source code for QVideo.lib.QVideoReader

'''Abstract base class for video file readers.'''
from abc import ABCMeta, abstractmethod
from types import TracebackType
from qtpy import QtCore
from QVideo.lib import QCamera
import QVideo
from pathlib import Path
import logging


logger = logging.getLogger(__name__)

__all__ = ['QVideoReader']


class QVideoReaderMeta(type(QtCore.QObject), ABCMeta):
    pass


[docs] class QVideoReader(QtCore.QObject, metaclass=QVideoReaderMeta): '''Abstract base class for video-file readers. Provides a unified interface for reading frames from a video file, including rate-limited frame delivery and random access via :meth:`seek`. Pause/resume control is the responsibility of the enclosing :class:`~QVideo.lib.QVideoSource`. Subclasses implement :meth:`_initialize`, :meth:`_deinitialize`, :meth:`read`, and the abstract properties :attr:`fps`, :attr:`width`, :attr:`height`, :attr:`framenumber`, and :attr:`length`, as well as the :meth:`seek` method. Parameters ---------- filename : str Path to the video file to open. Signals ------- shapeChanged(QSize) Emitted when the file is opened and the frame dimensions are known. Notes ----- :meth:`saferead` paces frame delivery by sleeping :attr:`delay` milliseconds between reads so that callers receive frames at approximately the correct playback rate. ''' #: Emitted when the file is opened and the frame dimensions are known. shapeChanged = QtCore.Signal(QtCore.QSize) def __init__(self, filename: str) -> None: '''Initialise and open the video reader. Parameters ---------- filename : str Path to the video file. ''' super().__init__() self.filename = filename self._isopen = False self.open() def __enter__(self) -> 'QVideoReader': return self.open() def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None: self.close()
[docs] @QtCore.Slot() def open(self) -> 'QVideoReader': '''Open the video file. Calls :meth:`_initialize` only if not already open. Emits :attr:`shapeChanged` on success and logs a warning on failure. Returns ------- QVideoReader ``self``, to allow chaining. ''' if not self._isopen: self._isopen = bool(self._initialize()) if self._isopen: self.shapeChanged.emit(self.shape) else: logger.warning(f'{type(self).__name__}: initialization failed') return self
[docs] @QtCore.Slot() def close(self) -> None: '''Close the video file. Safe to call on an already-closed reader. ''' if self._isopen: self._deinitialize() self._isopen = False
[docs] def isOpen(self) -> bool: '''Return whether the file is currently open.''' return self._isopen
@abstractmethod def _initialize(self) -> bool: '''Open the video file so that :meth:`read` will succeed. Returns ------- bool ``True`` if the file was opened successfully. ''' @abstractmethod def _deinitialize(self) -> None: '''Close the video file so that deletion or re-opening succeeds.'''
[docs] @abstractmethod def read(self) -> QCamera.CameraData: '''Read the next frame from the video file. Returns ------- tuple[bool, ndarray or None] ``(True, frame)`` on success, ``(False, None)`` at end-of-file or on error. '''
[docs] def saferead(self) -> QCamera.CameraData: '''Read one frame, pacing delivery to the file's native frame rate. Blocks for :attr:`delay` milliseconds before each read so that callers receive frames at approximately the correct playback rate. Returns ------- tuple[bool, ndarray or None] Result of :meth:`read`. ''' QtCore.QThread.msleep(self.delay) return self.read()
@property @abstractmethod def fps(self) -> float: '''Frame rate of the video file [frames per second].''' @property @abstractmethod def length(self) -> int: '''Total number of frames in the video file.''' @property @abstractmethod def framenumber(self) -> int: '''Current frame index (zero-based).''' @property @abstractmethod def width(self) -> int: '''Frame width in pixels.''' @property @abstractmethod def height(self) -> int: '''Frame height in pixels.''' @property def delay(self) -> int: '''Inter-frame delay in milliseconds, derived from :attr:`fps`.''' return int(1000. / self.fps) @property def shape(self) -> QtCore.QSize: '''Frame dimensions as ``QSize(width, height)``.''' return QtCore.QSize(int(self.width), int(self.height))
[docs] @QtCore.Slot(int) @abstractmethod def seek(self, framenumber: int) -> None: '''Seek to the specified frame. Parameters ---------- framenumber : int Target frame index (zero-based). '''
[docs] @QtCore.Slot() def rewind(self) -> None: '''Seek to the first frame.''' self.seek(0)
[docs] @staticmethod def examplevideo() -> str: # pragma: no cover '''Return the path to the bundled example video file.''' path = Path(QVideo.__file__).parent / 'docs' / 'diatom3.avi' return str(path)
[docs] @classmethod def example(cls: type['QVideoReader']) -> None: # pragma: no cover '''Print file metadata and read a few frames.''' filename = cls.examplevideo() video = cls(filename) print(filename) print(f'{video.length = } frames') print(f'{video.width = } pixels') print(f'{video.height = } pixels') print(f'{video.fps = } fps') video.close() with video: for _ in range(5): ok, frame = video.read() print(f'{video.framenumber} ', end='') print('done') with video: for _ in range(5): ok, frame = video.read() print(f'{video.framenumber} ', end='') print('done')